diff --git a/.bandit.yml b/.bandit.yml deleted file mode 100644 index 55c6741..0000000 --- a/.bandit.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -skips: [] -# No need to check for security issues in the test scripts! -exclude_dirs: - - "./tests/" diff --git a/.dockerignore b/.dockerignore index 54c30e3..34353d4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,3 +22,5 @@ LICENSE **/.vscode/ invoke*.yml tasks.py +clab-files +clab-* diff --git a/.flake8 b/.flake8 deleted file mode 100644 index e3ba27d..0000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -# E501: Line length is enforced by Black, so flake8 doesn't need to check it -# W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e77c28b..597bad2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,15 +1,12 @@ --- name: "CI" - -on: - push: - branches: ["*"] - pull_request: - branches: ["develop", "master"] +on: # yamllint disable-line rule:truthy rule:comments + - "push" + - "pull_request" jobs: linters: - name: linters + name: "Code Quality - Linting" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -23,25 +20,25 @@ jobs: - name: Lint & Code Format run: | - echo 'Running Flake8' && \ - poetry run flake8 . && \ + echo 'Rnning Ruff' && \ + poetry run ruff . && \ echo 'Running Black' && \ poetry run black --check --diff . && \ echo 'Running Yamllint' && \ poetry run yamllint . && \ - echo 'Running pydocstyle' && \ - poetry run pydocstyle . && \ echo 'Running Bandit' && \ - poetry run bandit --recursive ./ --configfile .bandit.yml && \ + poetry run bandit --recursive ./ --configfile pyproject.toml && \ echo 'Running MyPy' && \ poetry run mypy . test: name: Testing on Python ${{ matrix.python-version }} runs-on: ubuntu-latest + needs: + - "linters" strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v2 - name: Setup python @@ -50,8 +47,20 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 - - name: Start NETCONF Sysrepo container - run: docker-compose up -d netconf_sysrepo + - name: "Install Containerlab" + run: | + sudo bash -c "$(curl -sL https://get.containerlab.dev)" + + - name: "Start Arista CEOS" + run: "sudo containerlab deploy -t clab-files/clab-arista.yml" + + - name: "Wait for Arista CEOS to be ready" + uses: "jakejarvis/wait-action@master" + with: + time: "10" + + - name: "Change ownership of Containerlab files" + run: "sudo chown -R $USER clab-arista-testing.yml" - name: Install dependencies run: | diff --git a/.gitignore b/.gitignore index 8d7bbaa..c35d592 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,6 @@ docs/public/* # containerlab .clab-topo-netconf.yml.bak clab-clab-topo-netconf.yml/ +.clab-arista.yml.bak +clab-arista-testing.yml +test/test_data/schemas diff --git a/.pydocstyle.ini b/.pydocstyle.ini deleted file mode 100644 index 541cc51..0000000 --- a/.pydocstyle.ini +++ /dev/null @@ -1,11 +0,0 @@ -[pydocstyle] -convention = google -inherit = false -match = (?!__init__).*\.py -match-dir = (?!tests|migrations|development)[^\.].* -# D212 is enabled by default in google convention, and complains if we have a docstring like: -# """ -# My docstring is on the line after the opening quotes instead of on the same line as them. -# """ -# We've discussed and concluded that we consider this to be a valid style choice. -add_ignore = D212 \ No newline at end of file diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 38016ef..0000000 --- a/.pylintrc +++ /dev/null @@ -1,612 +0,0 @@ -[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-allow-list= - -# 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. (This is an alternative name to extension-pkg-allow-list -# for backward compatibility.) -extension-pkg-whitelist= - -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 - -# Files or directories to be skipped. They should be base names, not paths. -ignore=CVS,conftest.py - -# Files or directories matching the regex patterns are skipped. The regex -# matches against base names, not paths. -ignore-patterns=test_*. - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# 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, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - 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, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - too-many-arguments, - broad-except, - duplicate-code - -# 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 score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score 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, e.g. -# 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 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit,argparse.parse_error - - -[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 - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# 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 constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. -#class-const-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, - _ - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# 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. -# These decorators are taken in consideration only for invalid-name. -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=120 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# 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] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# 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 - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[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 work, -# install the 'python-enchant' package. -spelling-dict= - -# List of comma separated words that should be considered directives if they -# appear and the beginning of a comment and should not be checked. -spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no - -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=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 - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=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 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of names allowed to shadow builtins -allowed-redefined-builtins= - -# 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. expected to -# not be 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,builtins,io - - -[CLASSES] - -# Warn about protected attribute access inside special methods -check-protected-access-in-special-methods=no - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp, - __post_init__ - -# 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=cls - - -[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 an if statement (see R0916). -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] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# 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=optparse,tkinter.tix - -# Output a graph (.gv or any supported image format) of external dependencies -# to the given file (report RP0402 must not be disabled). -ext-import-graph= - -# Output a graph (.gv or any supported image format) of all (i.e. internal and -# external) dependencies to the given file (report RP0402 must not be -# disabled). -import-graph= - -# Output a graph (.gv or any supported image format) of internal dependencies -# to 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 - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception diff --git a/.yamllint.yml b/.yamllint.yml index 3c086bb..1f6c9e2 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -11,5 +11,5 @@ rules: ignore: | .github/ - clab-clab-topo-netconf.yml/ - .clab-topo-netconf.yaml + clab-files + clab-arista-testing.yml diff --git a/Dockerfile b/Dockerfile index fa734c5..882560a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,10 +10,8 @@ ENV PATH="/root/.local/bin:$PATH" RUN poetry config virtualenvs.create false -# Install project manifest COPY poetry.lock pyproject.toml ./ -# Install production dependencies RUN poetry install --no-root FROM base AS test @@ -22,23 +20,17 @@ COPY . . RUN poetry install --no-interaction -# Runs all necessary linting and code checks -RUN echo 'Running Flake8' && \ - flake8 . && \ +RUN echo 'Rnning Ruff' && \ + ruff . && \ echo 'Running Black' && \ black --check --diff . && \ echo 'Running Yamllint' && \ yamllint . && \ - echo 'Running Pylint' && \ - find . -name '*.py' | xargs pylint && \ - echo 'Running pydocstyle' && \ - pydocstyle . && \ echo 'Running Bandit' && \ - bandit --recursive ./ --configfile .bandit.yml && \ + bandit --recursive ./ --configfile pyproject.toml && \ echo 'Running MyPy' && \ mypy . -# Run full test suite including integration ENTRYPOINT ["pytest"] CMD ["--cov=nornir_netconf/", "tests/", "-vvv"] diff --git a/README.md b/README.md index 5c78236..8577390 100644 --- a/README.md +++ b/README.md @@ -26,22 +26,25 @@ pip install nornir_netconf --- -- **netconf_capabilities** - Return server capabilities from target -- **netconf_get** - Returns state data based on the supplied xpath -- **netconf_get_config** - Returns configuration from specified configuration store (default="running") -- **netconf_edit_config** - Edits configuration on specified datastore (default="running") -- **netconf_lock** - Locks or Unlocks a specified datastore (default="lock") -- **netconf_commit** - Commits a change +- **netconf_capabilities** - Return server capabilities from target -> `Result.result -> RpcResult` +- **netconf_commit** - Commits a change -> `Result.result -> RpcResult` +- **netconf_edit_config** - Edits configuration on specified datastore (default="running") -> `Result.result -> RpcResult` +- **netconf_get** - Returns state data based on the supplied xpath -> `Result.result -> RpcResult` +- **netconf_get_config** - Returns configuration from specified configuration store (default="running") -> `Result.result -> RpcResult` +- **netconf_get_schemas** - Retrieves schemas and saves aggregates content into a directory with schema output -> `Result.result -> SchemaResult` +- **netconf_lock** - Locks or Unlocks a specified datastore (default="lock") -> `Result.result -> RpcResult` +- **netconf_validate** - Validates configuration datastore. Requires the `validate` capability. -> `Result.result -> RpcResult` ## Response Result -The goal of the task results is to put the NETCONF RPC-reply back in your hands. An 'rpc' key will be available which can then be used to access 'data_xml' or 'xml' depending on the type of response or any other attributes available, such as 'error', 'errors'. Some of the RPC is unpacked and provided back as part of the Result by default, including the 'error', 'errors' and 'ok' if available. Anything else can be accessed directly from the rpc. +The goal of the task results is to put the NETCONF RPC-reply back in your hands. In most cases, the Nornir `Result.result` attribute will return back a `dataclass` depending on the task operation. It's important that you understand the object you will be working with. Please see the `dataclasses` section below and review the code if you want to see what attributes to expect. -Furthermore, some tasks allow the 'xml_dict' boolean argument. This will take the response RPC XML and convert it into a python dictionary. Keep in mind, this may not be perfect as XML doesn't quite translate 100% into a python dictionary. +### Dataclasses -For example, an xml response can include a collapsed response with open/close as so: `` If parsed into a python dictionary using xml_dict argument, the key of 'ok' will have a value of none. However, if we were to be parsing `True` this would show a key of 'enabled' and a value of 'True'. +> Defined in `nornir_netconf/plugins/helpers/models.py` -This is a simple built-in solution available, but not the only one. You have the RPC as part of the response and you are able to parse it anyway or method which works better for you. +- `RpcResult` -> This will return an attribute of `rpc` and `manager`. You will encounter this object in most Nornir `Results` as the return value to the `result` attribute. NETCONF / XML payloads can be overwhelming, especially with large configurations and it's just not efficient or useful to display thousands of lines of code in any result. +- `SchemaResult` -> An aggregation of interesting information when grabbing schemas from NETCONF servers. ## Global Lock @@ -70,7 +73,7 @@ Head over to the [Examples directory](https://github.com/h4ndzdatm0ld/nornir_net
Netconf Connection Plugin -Below is the snippet of a host inside the host-local.yml file and its associated group, 'sros'. +Below is the snippet of a host inside the host-local.yml file and its associated group, `sros`. ```yaml nokia_rtr: @@ -100,46 +103,70 @@ sros:
Task: Get Config ```python -"""Nornir NETCONF Example Task: 'get-config'.""" -from nornir import InitNornir -from nornir_utils.plugins.functions import print_result -from nornir_netconf.plugins.tasks import netconf_get_config + """Nornir NETCONF Example Task: 'get-config'.""" + from nornir import InitNornir + from nornir.core.task import Task + from nornir_utils.plugins.functions import print_result + from nornir_netconf.plugins.tasks import netconf_get_config -__author__ = "Hugo Tinoco" -__email__ = "hugotinoco@icloud.com" + __author__ = "Hugo Tinoco" + __email__ = "hugotinoco@icloud.com" -nr = InitNornir("config.yml") + nr = InitNornir("config.yml") -# Filter the hosts by 'west-region' assignment -west_region = nr.filter(region="west-region") + # Filter the hosts by 'west-region' assignment + west_region = nr.filter(region="west-region") -def example_netconf_get_config(task): - """Test get config.""" + def example_netconf_get_config(task: Task) -> str: + """Test get config.""" + config = task.run( + netconf_get_config, + source="running", + path=""" + + + Base + + + """, + filter_type="subtree", + ) + return config.result.rpc.data_xml - task.run( - netconf_get_config, - source="running", - path=""" - - - Base - - - """, - filter_type="subtree", - ) + def main(): + """Execute Nornir Script.""" + print_result(west_region.run(task=example_netconf_get_config)) -def main(): - """Execute Nornir Script.""" - print_result(west_region.run(task=example_netconf_get_config)) + if __name__ == "__main__": + main() +``` -if __name__ == "__main__": - main() +This returns the following +```bash + vvvv example_netconf_get_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + + + + + Base + + L3-OAM-eNodeB069420-X1 + disable + false + + + + + + ---- netconf_get_config ** changed : False ------------------------------------- INFO + RpcResult(rpc=) + ^^^^ END example_netconf_get_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + (nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feature/validate-tasks) ✗ ```
@@ -147,33 +174,50 @@ if __name__ == "__main__":
Task: Get Capabilities ```python -"""Nornir NETCONF Example Task: 'get-config'.""" -from nornir import InitNornir -from nornir_utils.plugins.functions import print_result -from nornir_netconf.plugins.tasks import netconf_capabilities + """Nornir NETCONF Example Task: 'capabilities'.""" + from nornir import InitNornir + from nornir.core.task import Task + from nornir_utils.plugins.functions import print_result + + from nornir_netconf.plugins.tasks import netconf_capabilities + __author__ = "Hugo Tinoco" + __email__ = "hugotinoco@icloud.com" -__author__ = "Hugo Tinoco" -__email__ = "hugotinoco@icloud.com" + nr = InitNornir("config.yml") -nr = InitNornir("config.yml") + # Filter the hosts by 'west-region' assignment + west_region = nr.filter(region="west-region") -# Filter the hosts by 'west-region' assignment -west_region = nr.filter(region="west-region") + def example_netconf_get_capabilities(task: Task) -> str: + """Test get capabilities.""" + capabilities = task.run(netconf_capabilities) + # This may be a lot, so for example we'll just print the first one + return [cap for cap in capabilities.result.rpc][0] -def example_netconf_get_capabilities(task): - """Test get capabilities.""" - task.run(netconf_capabilities) + def main(): + """Execute Nornir Script.""" + print_result(west_region.run(task=example_netconf_get_capabilities)) -def main(): - """Execute Nornir Script.""" - print_result(west_region.run(task=example_netconf_get_capabilities)) + if __name__ == "__main__": + main() +``` + +This returns the following -if __name__ == "__main__": - main() +```bash + (nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feature/validate-tasks) ✗ python3 nr_get_capabilities.py + example_netconf_get_capabilities************************************************ + * nokia_rtr ** changed : False ************************************************* + vvvv example_netconf_get_capabilities ** changed : False vvvvvvvvvvvvvvvvvvvvvvv INFO + urn:ietf:params:netconf:base:1.0 + ---- netconf_capabilities ** changed : False ----------------------------------- INFO + RpcResult(rpc=) + ^^^^ END example_netconf_get_capabilities ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + (nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feature/validate-tasks) ✗ ```
@@ -181,74 +225,135 @@ if __name__ == "__main__":
Task: Edit-Config with Global Lock ```python -"""Nornir NETCONF Example Task: 'edit-config', 'netconf_lock'.""" -from nornir import InitNornir -from nornir_utils.plugins.functions import print_result -from nornir_netconf.plugins.tasks import netconf_edit_config, netconf_lock, netconf_commit + """Nornir NETCONF Example Task: 'edit-config', 'netconf_lock'.""" + from nornir import InitNornir + from nornir_utils.plugins.functions import print_result + from nornir_netconf.plugins.tasks import netconf_edit_config, netconf_lock, netconf_commit + + + __author__ = "Hugo Tinoco" + __email__ = "hugotinoco@icloud.com" + + nr = InitNornir("config.yml") + + # Filter the hosts by 'west-region' assignment + west_region = nr.filter(region="west-region") + + + def example_global_lock(task): + """Test global lock operation of 'candidate' datastore.""" + lock = task.run(netconf_lock, datastore="candidate", operation="lock") + # Retrieve the Manager(agent) from lock operation and store for further + # operations. + task.host["manager"] = lock.result.manager + + + def example_edit_config(task): + """Test edit-config with global lock using manager agent.""" + + config_payload = """ + + + + Base + + L3-OAM-eNodeB069420-X1 + disable + false + + + + + """ + + result = task.run( + netconf_edit_config, config=config_payload, target="candidate", manager=task.host["manager"] + ) + # Validate configuration + task.run(netconf_validate) + # Commit + task.run(netconf_commit, manager=task.host["manager"]) + + def example_unlock(task): + """Unlock candidate datastore.""" + task.run(netconf_lock, datastore="candidate", operation="unlock", manager=task.host["manager"]) + + + def main(): + """Execute Nornir Script.""" + print_result(west_region.run(task=example_global_lock)) + print_result(west_region.run(task=example_edit_config)) + print_result(west_region.run(task=example_unlock)) + + + if __name__ == "__main__": + main() +``` + +
-__author__ = "Hugo Tinoco" -__email__ = "hugotinoco@icloud.com" +
Task: Get Schemas -nr = InitNornir("config.yml") +```python + """Get Schemas from NETCONF device.""" + from nornir import InitNornir + from nornir.core import Task + from nornir.core.task import Result + from nornir_utils.plugins.functions import print_result -# Filter the hosts by 'west-region' assignment -west_region = nr.filter(region="west-region") + from nornir_netconf.plugins.tasks import netconf_get, netconf_get_schemas + from tests.conftest import xml_dict + __author__ = "Hugo Tinoco" + __email__ = "hugotinoco@icloud.com" -def example_global_lock(task): - """Test global lock operation of 'candidate' datastore.""" - lock = task.run(netconf_lock, datastore="candidate", operation="lock") - # Retrieve the Manager(agent) from lock operation and store for further - # operations. - task.host["manager"] = lock.result["manager"] + nr = InitNornir("config.yml") -def example_edit_config(task): - """Test edit-config with global lock using manager agent.""" + # Filter the hosts by 'west-region' assignment + west_region = nr.filter(region="west-region") - config_payload = """ - - - - Base - - L3-OAM-eNodeB069420-X1 - disable - false - - - - + SCHEMA_FILTER = """ + + + + """ - result = task.run( - netconf_edit_config, config=config_payload, target="candidate", manager=task.host["manager"], xmldict=True - ) - # Access the RPC response object directly. - # Or you can check the 'ok' attr from an rpc response as well, if it exists. - if "ok" in result.result["rpc"].data_xml: - task.run(netconf_commit, manager=task.host["manager"], xmldict=True) + def example_task_get_schemas(task: Task) -> Result: + """Get Schemas from NETCONF device.""" + result = task.run(netconf_get, path=SCHEMA_FILTER, filter_type="subtree") + # xml_dict is a custom function to convert XML to Python dictionary. Not part of Nornir Plugin. + # See the code example if you want to use it. + parsed = xml_dict(result.result.rpc.data_xml) + first_schema = parsed["rpc-reply"]["data"]["netconf-state"]["schemas"]["schema"][0] + return task.run(netconf_get_schemas, schemas=[first_schema["identifier"]], schema_path="./output/schemas") - # Check OK key exists, as we passed in 'xmldict=True' - print(result.result["xml_dict"].keys()) -def example_unlock(task): - """Unlock candidate datastore.""" - task.run(netconf_lock, datastore="candidate", operation="unlock", manager=task.host["manager"]) + def main(): + """Execute Nornir Script.""" + print_result(west_region.run(task=example_task_get_schemas)) -def main(): - """Execute Nornir Script.""" - print_result(west_region.run(task=example_global_lock)) - print_result(west_region.run(task=example_edit_config)) - print_result(west_region.run(task=example_unlock)) + if __name__ == "__main__": + main() +``` -if __name__ == "__main__": - main() +This returns the following +```bash + (nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feature/validate-tasks) ✗ python3 nr_get_schemas.py + example_task_get_schemas******************************************************** + * nokia_rtr ** changed : False ************************************************* + vvvv example_task_get_schemas ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO + ---- netconf_get ** changed : False -------------------------------------------- INFO + RpcResult(rpc=) + ---- netconf_get_schemas ** changed : False ------------------------------------ INFO + SchemaResult(directory='./output/schemas') + ^^^^ END example_task_get_schemas ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ```
@@ -256,21 +361,21 @@ if __name__ == "__main__": ## Additional Documentation - [NCClient](https://ncclient.readthedocs.io/en/latest/) -- [Sysrepo](https://www.sysrepo.org/) ## Contributions +> Github actions spins up a Containerlab instance to do full integration tests once linting has been satisfied. + --- No line of code shall go untested! Any contribution will need to be accounted for by the coverage report and satisfy all linting. Linters: -- Fake8 +- Ruff (Flake8/Pydocstyle) - Black - Yamllint - Pylint -- Pydocstyle - Bandit - MyPy @@ -298,12 +403,6 @@ export SKIP_INTEGRATION_TESTS=False docker-compose up -d ``` -If you do not want to run integration tests, only bring up the `netconf_sysrepo` docker service and continue - -```bash -docker-compose up netconf_sysrepo -d -``` - ```bash poetry install && poetry shell ``` @@ -314,31 +413,13 @@ pytest --cov=nornir_netconf --color=yes --disable-pytest-warnings -vvv ### Integration Tests -Devices with full integration tests (ContainerLab) +Devices with full integration tests with ContainerLab - Nokia SROS - TiMOS-B-21.2.R1 - Cisco IOSxR - Cisco IOS XR Software, Version 6.1.3 - -Devices testing against Always-ON Sandboxes (Cisco DevNet) - -- Cisco IOS-XE - Cisco IOS XE Software, Version 17.03.01a These tests are run locally. - -### Sysrepo: netopeer2 - -The majority of integration tests are run against a docker instance of [netopeer2](https://hub.docker.com/r/sysrepo/sysrepo-netopeer2) - -From the [Sysrepo](https://www.sysrepo.org/) website: - -"Netopeer2 and Sysrepo provide a fully open source and standards compliant implementation of a NETCONF server and YANG configuration data stores." +- Cisco IOSXE - Cisco IOS XE Software, Version 17.03.02 +- Arista CEOS - 4.28.0F-26924507.4280F (engineering build) ## Documentation Documentation is generated with Sphinx and hosted with Github Pages. [Documentation](https://h4ndzdatm0ld.github.io/nornir_netconf/) - -To generate the latest documentation locally: - -```bash -sphinx-build -vvv -b html ./docs ./docs/public -cd docs/public -python -m http.server -``` diff --git a/clab-files/clab-arista.yml b/clab-files/clab-arista.yml new file mode 100644 index 0000000..7e15c04 --- /dev/null +++ b/clab-files/clab-arista.yml @@ -0,0 +1,16 @@ +# clab/clab@123 +--- +name: "arista-testing.yml" +mgmt: + network: "nornir-netconf-testing-arista" + ipv4_subnet: "172.200.101.0/24" +topology: + kinds: + ceos: + image: "h4ndzdatm0ld/ceosimage:4.28.0F" + binds: + - "interfaces.json:/mnt/flash/EosIntfMapping.json:ro" + nodes: + ceos: + kind: "ceos" + mgmt_ipv4: "172.200.101.11" diff --git a/clab-topo-netconf.yml b/clab-files/clab-topo-netconf.yml similarity index 80% rename from clab-topo-netconf.yml rename to clab-files/clab-topo-netconf.yml index 008d27f..5af8523 100644 --- a/clab-topo-netconf.yml +++ b/clab-files/clab-topo-netconf.yml @@ -11,6 +11,8 @@ topology: image: "h4ndzdatm0ld/vr-xrv:6.1.3" vr-sros: image: "h4ndzdatm0ld/sros:latest" + vr-csr: + image: "h4ndzdatm0ld/vr-csr:17.03.02" nodes: xrv-p1: kind: "vr-xrv" @@ -18,6 +20,9 @@ topology: sros-p2: kind: "vr-sros" mgmt_ipv4: "172.200.100.12" + vr-csr-1: + kind: "vr-csr" + mgmt_ipv4: "172.200.100.13" links: # p1 XR port 1 is connected to p2 SROS port 1 - endpoints: ["xrv-p1:eth1", "sros-p2:eth1"] diff --git a/clab-files/interfaces.json b/clab-files/interfaces.json new file mode 100644 index 0000000..1b062ca --- /dev/null +++ b/clab-files/interfaces.json @@ -0,0 +1,13 @@ +{ + "ManagementIntf": { + "eth0": "Management1" + }, + "EthernetIntf": { + "eth1": "Ethernet1/1", + "eth2": "Ethernet2/1", + "eth3": "Ethernet27/1", + "eth4": "Ethernet28/1", + "eth5": "Ethernet3/1/1", + "eth6": "Ethernet5/2/1" + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 985e58b..2d6ff6c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,39 +1,30 @@ --- version: "3.8" services: - netconf_sysrepo: - container_name: "netconf_sysrepo" - hostname: "netconf_sysrepo" - image: "sysrepo/sysrepo-netopeer2:latest" - ports: - - "1830:830" - environment: - NACM_RECOVERY_UID: "0" test: network_mode: "host" container_name: "test" hostname: "test" environment: SKIP_INTEGRATION_TESTS: "${SKIP_INTEGRATION_TESTS:-True}" + NORNIR_LOG: "${NORNIR_LOG:-False}" build: context: "." target: "test" - depends_on: - - "netconf_sysrepo" volumes: - "./:/usr/src/app" clab: image: "ghcr.io/srl-labs/clab" - network_mode: "host" working_dir: "/src" + network_mode: "host" volumes: - "/var/run/docker.sock:/var/run/docker.sock" - "/var/run/netns:/var/run/netns" - "/etc/hosts:/etc/hosts" - "./:/src" - - "./clab-topo-netconf.yml:/src/clab-topo-netconf.yml" pid: "host" - command: "containerlab deploy -t clab-topo-netconf.yml" + # command: "containerlab deploy -t ./clab-files/clab-topo-netconf.yml" + command: "containerlab deploy -t ./clab-files/clab-arista.yml --reconfigure" # If you want to destroy the lab, ovveride the command while executing docker-compose service # docker-compose run clab containerlab destroy -t clab-topo-netconf.yml privileged: true diff --git a/docs/additional/dataclasses.md b/docs/additional/dataclasses.md new file mode 100644 index 0000000..9d955a3 --- /dev/null +++ b/docs/additional/dataclasses.md @@ -0,0 +1,34 @@ +# DataClasses Implementation + +As of version 2.0, there will be an introduction of `RpcResult` and `SchemaResult`. Going forward, any task will return a dataclass to ensure a good experience for the developers and users of this project. + +Please view the source code to ensure this is the most update to date information on the implementations. + +> SOURCE: `nornir_netconf/plugins/helpers/rpc.py` + +## RpcResult + +This will be the object that will mostly be presented back to users as the return value to the `Result.result` attribute. + +```python +@dataclass +class RpcResult: + """RPC Reply Result Model.""" + + rpc: Optional[RPCReply] = field(default=None, repr=True) + manager: Optional[Manager] = field(default=None, repr=False) +``` + +## SchemaResult + +This will provide users with information about valid schemas which were created and in what `files` they were outputted to. Additionally, the `directory` in which the files where aggregated and written to. if any errors were encountered during the writing of the files or retrieval of the schema, they will be aggregated into the `errors` attribute. + +```python +@dataclass +class SchemaResult: + """Get Schema Result.""" + + directory: str = field(repr=True) + errors: List[str] = field(repr=False, default_factory=list) + files: List[str] = field(repr=False, default_factory=list) +``` diff --git a/docs/additional/rpc.md b/docs/additional/rpc.md index 8fd6ea9..6786695 100644 --- a/docs/additional/rpc.md +++ b/docs/additional/rpc.md @@ -1,21 +1,14 @@ # Key Differences in RPC response objects -Different vendor implementations return back different attributes in the RPC response. This has become somewhat of an issue, but this plugin attempts at normalizing the Nornir `Result` object to include the following keys, always: +Different vendor implementations return back different attributes in the RPC response. -- error -- errors -- ok -- rpc +The `ok` response is not always a present attribute, unfortunately. The `data_xml` or `xml` attribute could be parsed to find this XML representation at times -The `Error/Errors` are only present in the Nokia SROS devices (as far as I am concerned). +The `rpc` attribute that's part of the `Result.RpcResult` response object is the actual RPC response from the server. -The `ok` response is determined based on the result from an RPC response after being evaluated. +Lets compare the attributes from an `SROS`device and a `Cisco IOSXR` device. The following shows the attributes and the type for the RPC object. -The `rpc` is the literall RPC Object. Sometimes, the __str__ implementation returns back a string output of the XML response. However, in the case of an SROS device response, the `rpc` key is an NCElement, which can be accessed directly. Same goes for other RPC objects, they may display the string output but within the `Result` object in a task, you are able to access the `rpc` attributes. - -Lets compare the attributes from an SROS device and a Cisco IOSXR device. The following shows the attributes and the `Result` object. - -Nokia SROS 7750 +`Nokia SROS 7750` ```py ['_NCElement__doc', '_NCElement__huge_tree', '_NCElement__result', '_NCElement__transform_reply', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'data_xml', 'find', 'findall', 'findtext', 'remove_namespaces', 'tostring', 'xpath'] @@ -27,7 +20,7 @@ Nokia SROS 7750 ``` -Cisco IOSxR +`Cisco IOSxR` ```py ['ERROR_CLS', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_errors', '_huge_tree', '_parsed', '_parsing_error_transform', '_parsing_hook', '_raw', '_root', 'error', 'errors', 'ok', 'parse', 'set_parsing_error_transform', 'xml'] diff --git a/docs/additional/use-examples.md b/docs/additional/use-examples.md index 5814c58..56e24fb 100644 --- a/docs/additional/use-examples.md +++ b/docs/additional/use-examples.md @@ -1,6 +1,6 @@ # How to use the `Examples` directory -The Examples directory contains a project folder that's setup to quickly test some functionalities of Nornir Netconf Plugin. This presents the users the ability to execute tasks and see how the plugin responds. +The `examples` directory contains a project folder that's setup to quickly test some functionalities of `NORNIR Netconf` Plugin. This presents the users and/or developers the ability to execute tasks and see how the plugin responds. However, this plugin has tons of tests so feel free to experiment. Start the ContainerLab Nodes. @@ -28,17 +28,17 @@ From the `examples-project` directory, execute a script against the Nokia SROS d ``` ```bash -(nornir-netconf-Ky5gYI2O-py3.9) ➜ example-project git:(sros-integration) ✗ python nr_get_config.py +(nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feat/docs/update) ✗ python3 nr_get_config.py example_netconf_get_config****************************************************** * nokia_rtr ** changed : False ************************************************* vvvv example_netconf_get_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO - + Base - L3-OAM-eNodeB069420-W1 + L3-OAM-eNodeB069420-X1 disable false @@ -47,8 +47,7 @@ vvvv example_netconf_get_config ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvv ---- netconf_get_config ** changed : False ------------------------------------- INFO -{ 'error': {}, - 'errors': [], - 'ok': None, - 'rpc': } +RpcResult(rpc=) +^^^^ END example_netconf_get_config ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +(nornir-netconf-Ky5gYI2O-py3.10) ➜ example-project git:(feat/docs/update) ✗ ``` diff --git a/docs/changelog.md b/docs/changelog.md index c6d530f..f669baa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -4,6 +4,35 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.0.0] - 2022-03-14 + +### Added + +- `netconf_validate` task has been implemented. This is tested in integration against SROS devices that support the capability. +- `ruff` linter +- Integration tests for IOSXE with ContainerLab. (Not in CI) +- Standardized on tests for all platforms part of integration tests. Added a common section for common NETCONF operations. +- Added examples and updated previous ones + +### Changed + +- `sysrepo` tests all got replaced by a containerized instance of Arista CEOS +- `mypy` settings were moved into pyproject.toml file +- `get_schema` doesn't pull `.data_xml` attribute and just dumps the output. +- `write_output` internal helper allows for custom file extension (used for yang schema dumps) +- `pylint` is now driven by `ruff` +- `bandit` is now configured by pyproject.toml | updated docker file for linter + github CI +- Added `is_truthy` helper and refactored `SKIP_INTEGRATION_TESTS` + +### Removed + +- Dropped Python3.7 - Only 3.8 and above is supported. +- `sysrepo` container and dependencies. No tests or reliance on this container anymore. +- `xmltodict` library has been removed. The user should parse XML as they please. +- `Flake8` (Replaced by `Ruff` as a plugin) +- `Pydocstyle` (Replaced by `Ruff` as a plugin) +- `pylint` (Replaced by `Ruff` as a plugin) + ## [1.1.0] - 2022-10-06 ### Added diff --git a/docs/conf.py b/docs/conf.py index 86dda80..c82fc16 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,9 +12,6 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) from shutil import copyfile # -- Project information ----------------------------------------------------- @@ -31,7 +28,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["autoapi.extension", "myst_parser"] # Document Python Code +extensions = ["autoapi.extension", "myst_parser", "sphinx.ext.napoleon"] autoapi_type = "python" autoapi_dirs = ["../nornir_netconf/"] @@ -48,7 +45,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = "sphinx_rtd_theme" +html_theme = "sphinx_pdj_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, diff --git a/examples/example-project/nr_edit_config_global_lock.py b/examples/example-project/nr_edit_config_global_lock.py index b259e2c..89b914a 100644 --- a/examples/example-project/nr_edit_config_global_lock.py +++ b/examples/example-project/nr_edit_config_global_lock.py @@ -1,12 +1,14 @@ # type: ignore -"""Nornir NETCONF Example Task: 'edit-config', 'netconf_lock'.""" +"""Nornir NETCONF Example Task: 'edit-config', 'netconf_lock', `netconf_commit`, and `netconf_validate""" from nornir import InitNornir +from nornir.core.task import Result, Task from nornir_utils.plugins.functions import print_result from nornir_netconf.plugins.tasks import ( netconf_commit, netconf_edit_config, netconf_lock, + netconf_validate, ) __author__ = "Hugo Tinoco" @@ -18,15 +20,15 @@ west_region = nr.filter(region="west-region") -def example_global_lock(task): +def example_global_lock(task: Task) -> Result: """Test global lock operation of 'candidate' datastore.""" lock = task.run(netconf_lock, datastore="candidate", operation="lock") # Retrieve the Manager(agent) from lock operation and store for further # operations. - task.host["manager"] = lock.result["manager"] + task.host["manager"] = lock.result.manager -def example_edit_config(task): +def example_edit_config(task: Task) -> Result: """Test edit-config with global lock using manager agent.""" config_payload = """ @@ -43,17 +45,14 @@ def example_edit_config(task): """ - result = task.run( - netconf_edit_config, config=config_payload, target="candidate", manager=task.host["manager"], xmldict=True - ) + task.run(netconf_edit_config, config=config_payload, target="candidate", manager=task.host["manager"]) + # Validate the candidate configuration + task.run(netconf_validate) + # Commit configuration + task.run(netconf_commit, manager=task.host["manager"]) - # Access the RPC response object directly. - # Or you can check the 'ok' attr from an rpc response as well, if it exists. - if "ok" in result.result["rpc"].data_xml: - task.run(netconf_commit, manager=task.host["manager"], xmldict=True) - -def example_unlock(task): +def example_unlock(task: Task) -> Result: """Unlock candidate datastore.""" task.run(netconf_lock, datastore="candidate", operation="unlock", manager=task.host["manager"]) diff --git a/examples/example-project/nr_get_capabilities.py b/examples/example-project/nr_get_capabilities.py index 1a1bcc4..0918f85 100644 --- a/examples/example-project/nr_get_capabilities.py +++ b/examples/example-project/nr_get_capabilities.py @@ -1,6 +1,7 @@ # type: ignore """Nornir NETCONF Example Task: 'capabilities'.""" from nornir import InitNornir +from nornir.core.task import Task from nornir_utils.plugins.functions import print_result from nornir_netconf.plugins.tasks import netconf_capabilities @@ -14,9 +15,11 @@ west_region = nr.filter(region="west-region") -def example_netconf_get_capabilities(task): +def example_netconf_get_capabilities(task: Task) -> str: """Test get capabilities.""" - task.run(netconf_capabilities) + capabilities = task.run(netconf_capabilities) + # This may be a lot, so for example we'll just print the first one + return [cap for cap in capabilities.result.rpc][0] def main(): diff --git a/examples/example-project/nr_get_config.py b/examples/example-project/nr_get_config.py index 3ddf360..239e6fa 100644 --- a/examples/example-project/nr_get_config.py +++ b/examples/example-project/nr_get_config.py @@ -1,6 +1,7 @@ # type: ignore """Nornir NETCONF Example Task: 'get-config'.""" from nornir import InitNornir +from nornir.core.task import Task from nornir_utils.plugins.functions import print_result from nornir_netconf.plugins.tasks import netconf_get_config @@ -14,9 +15,9 @@ west_region = nr.filter(region="west-region") -def example_netconf_get_config(task): +def example_netconf_get_config(task: Task) -> str: """Test get config.""" - data = task.run( + config = task.run( netconf_get_config, source="running", path=""" @@ -28,7 +29,7 @@ def example_netconf_get_config(task): """, filter_type="subtree", ) - return data.result["rpc"].data_xml + return config.result.rpc.data_xml def main(): diff --git a/examples/example-project/nr_get_schemas.py b/examples/example-project/nr_get_schemas.py new file mode 100644 index 0000000..3b10bf4 --- /dev/null +++ b/examples/example-project/nr_get_schemas.py @@ -0,0 +1,44 @@ +# type: ignore +"""Get Schemas from NETCONF device.""" +from nornir import InitNornir +from nornir.core import Task +from nornir.core.task import Result +from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import netconf_get, netconf_get_schemas +from tests.conftest import xml_dict + +__author__ = "Hugo Tinoco" +__email__ = "hugotinoco@icloud.com" + +nr = InitNornir("config.yml") + + +# Filter the hosts by 'west-region' assignment +west_region = nr.filter(region="west-region") + +SCHEMA_FILTER = """ + + + + +""" + + +def example_task_get_schemas(task: Task) -> Result: + """Get Schemas from NETCONF device.""" + result = task.run(netconf_get, path=SCHEMA_FILTER, filter_type="subtree") + # xml_dict is a custom function to convert XML to Python dictionary. Not part of Nornir Plugin. + # See the code example if you want to use it. + parsed = xml_dict(result.result.rpc.data_xml) + first_schema = parsed["rpc-reply"]["data"]["netconf-state"]["schemas"]["schema"][0] + return task.run(netconf_get_schemas, schemas=[first_schema["identifier"]], schema_path="./output/schemas") + + +def main(): + """Execute Nornir Script.""" + print_result(west_region.run(task=example_task_get_schemas)) + + +if __name__ == "__main__": + main() diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 27d6dcc..0000000 --- a/mypy.ini +++ /dev/null @@ -1,26 +0,0 @@ -# Global options: -[mypy] -ignore_errors = False -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -disallow_untyped_decorators = True -check_untyped_defs = True -disallow_any_generics = True -ignore_missing_imports = True -strict_optional = True -warn_unused_ignores = True -warn_return_any = True -python_version = 3.8 -warn_unused_configs = True -warn_redundant_casts = True -disallow_subclassing_any = True -no_implicit_optional = True -implicit_reexport = True -strict_equality = True -exclude = tests/ -show_error_codes = True - -[mypy-nornir_netconf.plugins.connections] -ignore_errors = False -warn_return_any = True diff --git a/nornir_netconf/plugins/connections/netconf.py b/nornir_netconf/plugins/connections/netconf.py index 7074761..7349ff7 100644 --- a/nornir_netconf/plugins/connections/netconf.py +++ b/nornir_netconf/plugins/connections/netconf.py @@ -2,7 +2,6 @@ from typing import Any, Dict, Optional from ncclient import manager -from nornir.core.configuration import Config from nornir_netconf.plugins.helpers import check_file @@ -89,22 +88,21 @@ class Netconf: def open( self, - hostname: Optional[str], - username: Optional[str], + hostname: str, + username: str, password: Optional[str], - port: Optional[int], + port: Optional[int] = 830, platform: Optional[str] = "default", extras: Optional[Dict[str, Any]] = None, - configuration: Optional[Config] = None, + configuration: Optional[Dict[str, Any]] = None, ) -> None: """Open NETCONF connection.""" - extras = extras or {} - + extras = extras if extras is not None else {} parameters: Dict[str, Any] = { "host": hostname, "username": username, "password": password, - "port": port or 830, + "port": port, "device_params": {"name": platform if platform else "default"}, } ssh_config_file = extras.get("ssh_config", configuration.ssh.config_file) # type: ignore[union-attr] diff --git a/nornir_netconf/plugins/helpers/__init__.py b/nornir_netconf/plugins/helpers/__init__.py index 048a712..6477f42 100644 --- a/nornir_netconf/plugins/helpers/__init__.py +++ b/nornir_netconf/plugins/helpers/__init__.py @@ -1,5 +1,6 @@ """Helper Functions.""" from .general import check_file, create_folder, write_output -from .rpc_helpers import check_capability, get_result, unpack_rpc, xml_to_dict +from .models import RpcResult, SchemaResult +from .rpc import check_capability -__all__ = ("unpack_rpc", "xml_to_dict", "get_result", "check_file", "write_output", "create_folder", "check_capability") +__all__ = ("RpcResult", "check_file", "write_output", "create_folder", "check_capability", "SchemaResult") diff --git a/nornir_netconf/plugins/helpers/general.py b/nornir_netconf/plugins/helpers/general.py index e5087a7..f30bfdd 100644 --- a/nornir_netconf/plugins/helpers/general.py +++ b/nornir_netconf/plugins/helpers/general.py @@ -30,7 +30,7 @@ def create_folder(directory: str) -> None: logging.info("Error when creating %s, %s", directory, err_ex) -def write_output(text: str, path: str, filename: str) -> None: +def write_output(text: str, path: str, filename: str, ext: str = "txt") -> None: """Take input and path and write a file. Args: @@ -40,5 +40,5 @@ def write_output(text: str, path: str, filename: str) -> None: """ if not os.path.isdir(path): create_folder(path) - with open(f"{path}/{filename}.txt", "w+", encoding="utf-8") as file: + with open(f"{path}/{filename}.{ext}", "w+", encoding="utf-8") as file: file.write(str(text)) diff --git a/nornir_netconf/plugins/helpers/models.py b/nornir_netconf/plugins/helpers/models.py new file mode 100644 index 0000000..d876947 --- /dev/null +++ b/nornir_netconf/plugins/helpers/models.py @@ -0,0 +1,23 @@ +"""Data Models.""" +from dataclasses import dataclass, field +from typing import List, Optional + +from ncclient.manager import Manager +from ncclient.operations.rpc import RPCReply + + +@dataclass +class RpcResult: + """RPC Reply Result Model.""" + + rpc: Optional[RPCReply] = field(default=None, repr=True) + manager: Optional[Manager] = field(default=None, repr=False) + + +@dataclass +class SchemaResult: + """Get Schema Result.""" + + directory: str = field(repr=True) + errors: List[str] = field(repr=False, default_factory=list) + files: List[str] = field(repr=False, default_factory=list) diff --git a/nornir_netconf/plugins/helpers/rpc.py b/nornir_netconf/plugins/helpers/rpc.py new file mode 100644 index 0000000..6078482 --- /dev/null +++ b/nornir_netconf/plugins/helpers/rpc.py @@ -0,0 +1,7 @@ +"""Helper to extract info from RPC reply.""" +from typing import List + + +def check_capability(capabilities: List[str], capability: str) -> bool: + """Evaluate capabilities and return True if capability is available.""" + return any(True for cap in capabilities if capability in cap) diff --git a/nornir_netconf/plugins/helpers/rpc_helpers.py b/nornir_netconf/plugins/helpers/rpc_helpers.py deleted file mode 100644 index e9aa503..0000000 --- a/nornir_netconf/plugins/helpers/rpc_helpers.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Helper to extract info from RPC reply.""" -from typing import Any, Dict, List, Union - -import xmltodict -from ncclient.operations.rpc import RPCReply - - -def check_capability(capabilities: List[str], capability: str) -> bool: - """Evaluate capabilities and return True if capability is available.""" - return any(True for cap in capabilities if capability in cap) - - -def xml_to_dict(rpc: RPCReply) -> Union[Any, Dict[str, str]]: - """Convert XML from RPC reply to dict. - - Args: - rpc (RPCReply): RPC Reply from Netconf Server - - Returns: - Dict: xml response -> Dict - """ - if hasattr(rpc, "data_xml"): - try: - return xmltodict.parse(rpc.data_xml) - except Exception as err_ex: - return {"error": f"Unable to parse XML to Dict. {err_ex}."} - elif hasattr(rpc, "xml"): - try: - return xmltodict.parse(rpc.xml) - except Exception as err_ex: - return {"error": f"Unable to parse XML to Dict. {err_ex}."} - else: - return {"error": "Unable to parse XML to Dict. '.xml' or 'data_xml' not found."} - - -def unpack_rpc(rpc: RPCReply, xmldict: bool = False) -> Dict[str, Union[RPCReply, str]]: - """Extract RPC attrs of interest. - - Args: - rpc (RPCReply): RPC Reply from Netconf Server - xmldict (boolean): convert xml to dict - - Return: - Dict: "RPC Attributes" - """ - result = { - "error": rpc.error, - "errors": rpc.errors, - "ok": rpc.ok, - "rpc": rpc, - } - - if xmldict: - result["xml_dict"] = xml_to_dict(rpc) - - return result - - -def get_result(rpc: Union[RPCReply, Dict[str, Any]], xmldict: bool = False) -> Dict[str, Union[RPCReply, str]]: - """Check if RPC reply is valid and unpack. - - Args: - rpc (Union[RPCReply, Dict]): RPC Reply from Netconf Server or Dict - xmldict (boolean): convert xml to dict - - Returns: - Dict: Results dict to expand in Result object - """ - # The RPCReply may vary in attributes it contains within the object. Sometimes, the 'ok' response - # could be missing. In order to standardize a similar result we evaluate the response and - # make adjustment where necessary to keep responses somewhat consistent without assumptions. - - result: Dict[str, Any] = {"error": {}, "errors": []} - if not isinstance(rpc, Dict): - # RPC will either have 'ok' or 'data_xml' attr: - if any(i for i in dir(rpc) if i in ["ok", "data_xml"]): - try: - if rpc.ok: - failed = False - else: - failed = True - return {"failed": failed, "result": unpack_rpc(rpc, xmldict)} - except AttributeError: - # Re-create `unpack_rpc` output keys to keep consistency. - result["rpc"] = rpc - result["ok"] = True if "" in rpc.data_xml else None - if xmldict: - result["xml_dict"] = xml_to_dict(rpc) - return {"failed": False, "result": result} - - # Safe to say, at this point the replies are not RPC or NCElements. - # So we can take advantage of passing dictionaries in and safe gets. - if isinstance(rpc, Dict): - result["error"] = rpc.get("error", {}) - result["errors"] = rpc.get("errors", "Unable to find 'ok' or data_xml in response object.") - result["ok"] = rpc.get("ok", False) - result["rpc"] = rpc.get("rpc", {}) - result["xml_dict"] = rpc.get("xml_dict", {}) - return {"failed": True, "result": result} diff --git a/nornir_netconf/plugins/tasks/__init__.py b/nornir_netconf/plugins/tasks/__init__.py index b1b1500..7284828 100644 --- a/nornir_netconf/plugins/tasks/__init__.py +++ b/nornir_netconf/plugins/tasks/__init__.py @@ -5,6 +5,7 @@ from .capabilities.netconf_capabilities import netconf_capabilities from .editing.netconf_commit import netconf_commit from .editing.netconf_edit_config import netconf_edit_config +from .editing.netconf_validate import netconf_validate from .locking.netconf_lock import netconf_lock from .retrieval.netconf_get import netconf_get from .retrieval.netconf_get_config import netconf_get_config @@ -18,4 +19,5 @@ "netconf_get_config", "netconf_lock", "netconf_get_schemas", + "netconf_validate", ) diff --git a/nornir_netconf/plugins/tasks/capabilities/netconf_capabilities.py b/nornir_netconf/plugins/tasks/capabilities/netconf_capabilities.py index ba1c1ae..00b117c 100644 --- a/nornir_netconf/plugins/tasks/capabilities/netconf_capabilities.py +++ b/nornir_netconf/plugins/tasks/capabilities/netconf_capabilities.py @@ -2,6 +2,7 @@ from nornir.core.task import Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME +from nornir_netconf.plugins.helpers import RpcResult def netconf_capabilities(task: Task) -> Result: @@ -13,10 +14,11 @@ def netconf_capabilities(task: Task) -> Result: > nr.run(task=netconf_capabilities) Returns: - Result object with the following attributes set: - * result (``list``): list with the capabilities of the host + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager """ - failed = False manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) - capabilities = list(manager.server_capabilities) - return Result(host=task.host, failed=failed, result=capabilities) + capabilities = manager.server_capabilities + rpc_result = RpcResult(rpc=capabilities, manager=manager) + return Result(host=task.host, result=rpc_result) diff --git a/nornir_netconf/plugins/tasks/editing/netconf_commit.py b/nornir_netconf/plugins/tasks/editing/netconf_commit.py index 72e8076..5c3b0c4 100644 --- a/nornir_netconf/plugins/tasks/editing/netconf_commit.py +++ b/nornir_netconf/plugins/tasks/editing/netconf_commit.py @@ -5,37 +5,41 @@ from nornir.core.task import Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers.rpc_helpers import get_result +from nornir_netconf.plugins.helpers import RpcResult def netconf_commit( task: Task, manager: Optional[Manager] = None, - xmldict: bool = False, - confirmed: bool = False, - timeout: int = 60, - persist: int = None, # type: ignore - persist_id: int = None, # type: ignore + confirmed: Optional[bool] = False, + timeout: Optional[int] = 60, + persist: Optional[int] = None, + persist_id: Optional[int] = None, ) -> Result: """Commit operation. Arguments: - manager: class:: ncclient.manager.Manager - xmldict (boolean): convert xml to dict + manager (Manager): NETCONF Manager confirmed (boolean): Commit confirm timeout (int): commit confirm timeout persist (int): survive a session termination persist_id (int): must equal given value of persist in original commit operation + Examples: Simple example:: + > nr.run(task=netconf_commit) + + With a carried manager session:: > nr.run(task=netconf_commit, manager=manager) Returns: - Result object with the following attributes set: - * result (``str``): The rpc-reply as an XML string + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager """ if not manager: manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) result = manager.commit(confirmed, timeout, persist, persist_id) - return Result(host=task.host, **get_result(result, xmldict)) + result = RpcResult(rpc=result, manager=manager) + return Result(host=task.host, result=result) diff --git a/nornir_netconf/plugins/tasks/editing/netconf_edit_config.py b/nornir_netconf/plugins/tasks/editing/netconf_edit_config.py index 181a0f8..0057943 100644 --- a/nornir_netconf/plugins/tasks/editing/netconf_edit_config.py +++ b/nornir_netconf/plugins/tasks/editing/netconf_edit_config.py @@ -5,35 +5,43 @@ from nornir.core.task import Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers.rpc_helpers import check_capability, get_result +from nornir_netconf.plugins.helpers import RpcResult, check_capability def netconf_edit_config( task: Task, config: str, - target: str = "running", + target: Optional[str] = "running", manager: Optional[Manager] = None, - xmldict: bool = False, - default_operation: Optional[str] = None, + default_operation: Optional[str] = "merge", ) -> Result: - """Edit configuration of device using Netconf. + """Edit configuration of the device using Netconf. Arguments: - config: Configuration snippet to apply - target: Target configuration store - manager: class:: ncclient.manager.Manager - xmldict (boolean): convert xml to dict - default_operation (str): merge, replace or None + config (str): Configuration snippet to apply + target (str): Target configuration store + manager (Manager): NETCONF Manager + default_operation (str): merge or replace Examples: Simple example:: > nr.run(task=netconf_edit_config, config=desired_config) + Changing Default Operation:: + + > nr.run(task=netconf_edit_config, config=desired_config, default_operation="replace") + + Changing Default Target of `running` to `candidate`:: + + > nr.run(task=netconf_edit_config, target="candidate", config=desired_config, default_operation="replace") + Returns: - Result + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager """ - if default_operation not in ["merge", "replace", None]: + if default_operation not in ["merge", "replace"]: raise ValueError(f"{default_operation} not supported.") if not manager: manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) @@ -42,4 +50,6 @@ def netconf_edit_config( if not check_capability(capabilities, target): raise ValueError(f"{target} datastore is not supported.") result = manager.edit_config(config, target=target, default_operation=default_operation) - return Result(host=task.host, **get_result(result, xmldict)) + + result = RpcResult(rpc=result, manager=manager) + return Result(host=task.host, result=result) diff --git a/nornir_netconf/plugins/tasks/editing/netconf_validate.py b/nornir_netconf/plugins/tasks/editing/netconf_validate.py new file mode 100644 index 0000000..a456341 --- /dev/null +++ b/nornir_netconf/plugins/tasks/editing/netconf_validate.py @@ -0,0 +1,36 @@ +"""NETCONF validate config.""" +from typing import Optional + +from ncclient.manager import Manager +from nornir.core.task import Result, Task + +from nornir_netconf.plugins.connections import CONNECTION_NAME +from nornir_netconf.plugins.helpers import RpcResult + + +def netconf_validate( + task: Task, + source: Optional[str] = "candidate", + manager: Optional[Manager] = None, +) -> Result: + """Validate the datastore configuration. + + Arguments: + source (str): Source configuration store + manager (Manager): NETCONF Manager + + Examples: + Simple example:: + + > nr.run(task=netconf_validate) + + Returns: + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager + """ + if not manager: + manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) + result = manager.validate(source=source) + rpc_result = RpcResult(rpc=result, manager=manager) + return Result(host=task.host, result=rpc_result) diff --git a/nornir_netconf/plugins/tasks/locking/netconf_lock.py b/nornir_netconf/plugins/tasks/locking/netconf_lock.py index 494a781..dc7a3f4 100644 --- a/nornir_netconf/plugins/tasks/locking/netconf_lock.py +++ b/nornir_netconf/plugins/tasks/locking/netconf_lock.py @@ -1,70 +1,66 @@ """NETCONF lock.""" +from typing import Optional + from ncclient.manager import Manager -from ncclient.operations.rpc import RPCError from nornir.core.task import Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers import get_result +from nornir_netconf.plugins.helpers import RpcResult -def netconf_lock(task: Task, datastore: str, manager: Manager = None, operation: str = "lock") -> Result: +def netconf_lock( + task: Task, + datastore: Optional[str] = "candidate", + manager: Optional[Manager] = None, + operation: str = "lock", +) -> Result: """NETCONF locking operations for a specified datastore. - By default, netconf_lock operations will display the 'data_xml' - extracted from the RPCReply of the server, as it should be mininal - data to display unlike other operations. - - Task name dynamically updated based on operation. + Task name dynamically updated based on operation of `lock` or `unlock`. Arguments: - datastore (str): Datastore to lock - manager (Manager): Manager to use if operation=='unlock' + datastore (str): Target Datastore + manager (Manager): Manager to use if operation=='unlock' and the lock is carried. operation (str): Unlock or Lock + Examples: Simple example:: > nr.run(task=netconf_lock) - lock candidate datestore:: + Lock candidate datestore:: > nr.run(task=netconf_lock, > operation="lock", > datastore="candidate") + Unlock candidate datestore:: + + > nr.run(task=netconf_lock, + > operation="unlock", + > datastore="candidate") + + Unlock candidate datestore with a session:: + + > task.run(task=netconf_lock, + > operation="unlock", + > datastore="candidate", + > manager=task.host["manager"]) + Returns: - Result object with the following attributes set: - * unpack_rpc (``dict``): - """ - result = {"failed": False, "result": {}} + Result object with the following attributes set:: + * result (RpcResult): Rpc and Manager + """ operation = operation.strip().lower() if operation not in ["lock", "unlock"]: - result["failed"] = True raise ValueError("Supported operations are: 'lock' or 'unlock'.") if not manager: manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) - try: - if operation == "lock": - result = manager.lock(target=datastore) - else: - result = manager.unlock(target=datastore) - task.name = "netconf_unlock" - except RPCError as err_ex: - result["error"] = err_ex - result["failed"] = True - - # Return the manager as part of the result. This can be used to pass into - # other functions, if a global lock is in place. Typically, you can extract - # session, session_id and use it. - - result_dict = get_result(result) - result_dict["result"]["manager"] = manager - - # Handle different responses & Update results - if hasattr(result, "data_xml"): - result_dict["result"]["data_xml"] = result.data_xml # type: ignore - elif hasattr(result, "xml"): - result_dict["result"]["data_xml"] = result.xml # type: ignore + if operation == "lock": + result = manager.lock(target=datastore) else: - result_dict["result"]["data_xml"] = None - return Result(host=task.host, **result_dict) + result = manager.unlock(target=datastore) + task.name = "netconf_unlock" + result = RpcResult(manager=manager, rpc=result) + return Result(host=task.host, result=result) diff --git a/nornir_netconf/plugins/tasks/retrieval/netconf_get.py b/nornir_netconf/plugins/tasks/retrieval/netconf_get.py index 2900fd0..61dd3d2 100644 --- a/nornir_netconf/plugins/tasks/retrieval/netconf_get.py +++ b/nornir_netconf/plugins/tasks/retrieval/netconf_get.py @@ -1,17 +1,16 @@ """NETCONF get.""" -from nornir.core.task import Result, Task +from nornir.core.task import Optional, Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers import get_result +from nornir_netconf.plugins.helpers import RpcResult -def netconf_get(task: Task, path: str = "", filter_type: str = "xpath", xmldict: bool = False) -> Result: - """Get information over Netconf from device. +def netconf_get(task: Task, path: Optional[str] = "", filter_type: Optional[str] = "xpath") -> Result: + """Get configuration and state information over Netconf from device. Arguments: - path: Subtree or xpath to filter - filter_type: Type of filtering to use, 'xpath' or 'subtree' - xmldict (boolean): convert xml to dict + path (Optional[str]): `Subtree` or `xpath` to filter + filter_type (Optional[str]): Type of filtering to use, `xpath or `subtree` Examples: Simple example:: @@ -20,27 +19,28 @@ def netconf_get(task: Task, path: str = "", filter_type: str = "xpath", xmldict: Passing options using ``xpath``:: - > query = "/devices/device" + > xpath = "/devices/device" > nr.run(task=netconf_get, - > path=query) + > path=xpath) - Passing options using ``subtree``:: + Passing options using ``subtree``:: - > query = "" + > subtree = "" > nr.run(task=netconf_get, > filter_type="subtree", - > path=query) + > path=subtree) Returns: - Result object with the following attributes set: - * result (``str``): The collected data as an XML string + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager """ params = {} - manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) - if path: params["filter"] = (filter_type, path) result = manager.get(**params) - return Result(host=task.host, **get_result(result, xmldict)) + + result = RpcResult(rpc=result, manager=manager) + return Result(host=task.host, result=result) diff --git a/nornir_netconf/plugins/tasks/retrieval/netconf_get_config.py b/nornir_netconf/plugins/tasks/retrieval/netconf_get_config.py index 15db6d6..e9ccec6 100644 --- a/nornir_netconf/plugins/tasks/retrieval/netconf_get_config.py +++ b/nornir_netconf/plugins/tasks/retrieval/netconf_get_config.py @@ -1,22 +1,22 @@ """NETCONF get config.""" -from typing import Any, Dict +from typing import Any, Dict, Optional from nornir.core.task import Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers.rpc_helpers import get_result +from nornir_netconf.plugins.helpers import RpcResult +# - > Prob don't need to add defaults. def netconf_get_config( - task: Task, source: str = "running", path: str = "", filter_type: str = "xpath", xmldict: bool = False + task: Task, source: Optional[str] = "running", path: Optional[str] = "", filter_type: Optional[str] = "xpath" ) -> Result: """Get configuration over Netconf from device. Arguments: - source: Configuration store to collect from - path: Subtree or xpath to filter - filter_type: Type of filtering to use, 'xpath' or 'subtree' - xmldict (boolean): convert xml to dict + source (Optional[str]): Configuration datastore to collect from. Defaults to `running` + path (Optional[str]): Subtree or xpath to filter. Defaults to `''` + filter_type (Optional[str]): Type of filtering to use, 'xpath' or 'subtree'. Defaults to `xpath` Examples: Simple example:: @@ -43,14 +43,15 @@ def netconf_get_config( Returns: - Result object with the following attributes set: - * result (``str``): The collected data as an XML string + Result object with the following attributes set:: + + * result (RpcResult): Rpc and Manager """ manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) - parameters: Dict[str, Any] = {"source": source} - + params: Dict[str, Any] = {"source": source} if path: - parameters["filter"] = (filter_type, path) + params["filter"] = (filter_type, path) + result = manager.get_config(**params) - result = manager.get_config(**parameters) - return Result(host=task.host, **get_result(result, xmldict)) + result = RpcResult(rpc=result, manager=manager) + return Result(host=task.host, result=result) diff --git a/nornir_netconf/plugins/tasks/retrieval/netconf_get_schemas.py b/nornir_netconf/plugins/tasks/retrieval/netconf_get_schemas.py index 2fe7277..d5d72b7 100644 --- a/nornir_netconf/plugins/tasks/retrieval/netconf_get_schemas.py +++ b/nornir_netconf/plugins/tasks/retrieval/netconf_get_schemas.py @@ -1,36 +1,40 @@ """NETCONF Schemas.""" -from typing import Dict - from ncclient.operations.rpc import RPCError from nornir.core.task import List, Result, Task from nornir_netconf.plugins.connections import CONNECTION_NAME -from nornir_netconf.plugins.helpers import write_output +from nornir_netconf.plugins.helpers import SchemaResult, write_output + + +def netconf_get_schemas(task: Task, schemas: List[str], schema_path: str) -> Result: # nosec + """Fetch provided schemas and write to a file inside of a given directory path, `schema_path`. + + All schemas will be written to a file in the `schema_path` directory provided and + named by the schema name. + Any errors on extracting the schema will be logged in the result object. -def netconf_get_schemas(task: Task, schemas: List[str], schema_path: str = "") -> Result: - """Fetch provided schemas and write to a file. + Args: + schemas (List[str]): List of schemas to fetch. + schema_path (str): Directory path to save schemas output. - Examples: - Simple example:: + Simple Example :: - > nr.run(task=netconf_schemas, schemas=["schema1", "schema2"], schema_path="/some/path") + > nr.run(task=netconf_schemas, schemas=["schema1", "schema2"], schema_path="workdir/schemas") Returns: - Result: Result + Result object with the following attributes set:: + + * result (SchemaResult): List of files created, errors, if any and base directory path. """ manager = task.host.get_connection(CONNECTION_NAME, task.nornir.config) - failed = False - result: Dict[str, List[str]] = {"errors": [], "log": []} - if schema_path: - for schema in schemas: - try: - write_output(manager.get_schema(schema).data_xml, path=schema_path, filename=schema) - result["log"].append(f"{schema_path}/{schema}.txt created.") - except RPCError as err_ex: - result["errors"].append(str(err_ex).strip()) - else: - failed = True - result["errors"].append("Missing 'schema_path' arg to save schema files.") - - return Result(host=task.host, failed=failed, result=result) + result = SchemaResult(directory=schema_path) + + for schema in schemas: + try: + write_output(manager.get_schema(schema), path=schema_path, filename=schema, ext="yang") + result.files.append(f"{schema_path}/{schema}.yang") + except RPCError as err_ex: + result.errors.append(str(err_ex).strip()) + + return Result(host=task.host, result=result) diff --git a/poetry.lock b/poetry.lock index ceafa41..c87d679 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,53 +1,57 @@ [[package]] name = "alabaster" -version = "0.7.12" +version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "astroid" -version = "2.11.7" +version = "2.15.0" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" -typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} -typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = ">=1.11,<2" +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +wrapt = [ + {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, + {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, +] [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +cov = ["attrs", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs"] +docs = ["furo", "sphinx", "myst-parser", "zope.interface", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["attrs", "zope.interface"] +tests-no-zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] +tests_no_zope = ["hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist", "cloudpickle", "mypy (>=0.971,<0.990)", "pytest-mypy-plugins"] [[package]] name = "babel" -version = "2.10.3" +version = "2.12.1" description = "Internationalization utilities" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -pytz = ">=2015.7" +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [[package]] name = "bandit" -version = "1.7.4" +version = "1.7.5" description = "Security oriented static analyser for python code." category = "dev" optional = false @@ -57,16 +61,17 @@ python-versions = ">=3.7" colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} GitPython = ">=1.0.1" PyYAML = ">=5.3.1" +rich = "*" stevedore = ">=1.20.0" [package.extras] -test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "toml", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)"] -toml = ["toml"] +test = ["coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)", "beautifulsoup4 (>=4.8.0)", "pylint (==1.9.4)", "tomli (>=1.1.0)"] +toml = ["tomli (>=1.1.0)"] yaml = ["pyyaml"] [[package]] name = "bcrypt" -version = "4.0.0" +version = "4.0.1" description = "Modern password hashing for your software and your servers" category = "main" optional = false @@ -78,7 +83,7 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "22.10.0" +version = "23.1.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -87,10 +92,10 @@ python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} -typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -101,7 +106,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "dev" optional = false @@ -120,14 +125,11 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "dev" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = ">=3.7.0" [[package]] name = "click" @@ -139,19 +141,18 @@ python-versions = ">=3.7" [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "coverage" -version = "6.5.0" +version = "7.2.1" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -165,7 +166,7 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "38.0.1" +version = "39.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -175,68 +176,55 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +pep8test = ["black", "ruff", "mypy", "types-pytz", "types-requests", "check-manifest"] sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] - -[[package]] -name = "dill" -version = "0.3.5.1" -description = "serialize all of python" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" - -[package.extras] -graph = ["objgraph (>=1.7.2)"] +test = ["pytest (>=6.2.0)", "pytest-shard (>=0.1.2)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test-randomorder = ["pytest-randomly"] +tox = ["tox"] [[package]] name = "docutils" -version = "0.17.1" +version = "0.19" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" [[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" +name = "exceptiongroup" +version = "1.1.1" +description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" +[package.extras] +test = ["pytest (>=6)"] [[package]] name = "gitdb" -version = "4.0.9" +version = "4.0.10" description = "Git Object Database" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] smmap = ">=3.0.1,<6" [[package]] name = "gitpython" -version = "3.1.28" -description = "GitPython is a python library used to interact with Git repositories" +version = "3.1.31" +description = "GitPython is a Python library used to interact with Git repositories" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] gitdb = ">=4.0.1,<5" -typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""} [[package]] name = "idna" @@ -263,7 +251,6 @@ optional = false python-versions = ">=3.7" [package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] @@ -273,24 +260,24 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "flake8 [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" [[package]] name = "isort" -version = "5.10.1" +version = "5.12.0" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.8.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] +colors = ["colorama (>=0.4.3)"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] [[package]] @@ -309,15 +296,15 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lazy-object-proxy" -version = "1.7.1" +version = "1.9.0" description = "A fast and thorough lazy object proxy." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "lxml" -version = "4.9.1" +version = "4.9.2" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -331,7 +318,7 @@ source = ["Cython (>=0.29.7)"] [[package]] name = "markdown-it-py" -version = "2.1.0" +version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" category = "dev" optional = false @@ -339,13 +326,12 @@ python-versions = ">=3.7" [package.dependencies] mdurl = ">=0.1,<1.0" -typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code_style = ["pre-commit (==2.6)"] -compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code_style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx-book-theme"] @@ -353,36 +339,12 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false python-versions = ">=3.7" -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mdit-py-plugins" -version = "0.3.1" -description = "Collection of plugins for markdown-it-py" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" - -[package.extras] -testing = ["pytest-regressions", "pytest-cov", "pytest", "coverage"] -rtd = ["sphinx-book-theme (>=0.1.0,<0.2.0)", "myst-parser (>=0.16.1,<0.17.0)", "attrs"] -code_style = ["pre-commit"] - [[package]] name = "mdurl" version = "0.1.2" @@ -393,7 +355,7 @@ python-versions = ">=3.7" [[package]] name = "mypy" -version = "0.982" +version = "1.0.1" description = "Optional static typing for Python" category = "dev" optional = false @@ -402,44 +364,21 @@ python-versions = ">=3.7" [package.dependencies] mypy-extensions = ">=0.4.3" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typed-ast = {version = ">=1.4.0,<2", markers = "python_version < \"3.8\""} typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] name = "mypy-extensions" -version = "0.4.3" +version = "0.4.4" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "main" optional = false -python-versions = "*" - -[[package]] -name = "myst-parser" -version = "0.18.1" -description = "An extended commonmark compliant parser, with bridges to docutils & sphinx." -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -docutils = ">=0.15,<0.20" -jinja2 = "*" -markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.1,<0.4.0" -pyyaml = "*" -sphinx = ">=4,<6" -typing-extensions = "*" - -[package.extras] -code_style = ["pre-commit (>=2.12,<3.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "sphinx-book-theme", "sphinx-design", "sphinxext-rediraffe (>=0.2.7,<0.3.0)", "sphinxcontrib.mermaid (>=0.7.1,<0.8.0)", "sphinxext-opengraph (>=0.6.3,<0.7.0)"] -testing = ["beautifulsoup4", "coverage", "pytest (>=6,<7)", "pytest-cov", "pytest-regressions", "pytest-param-files (>=0.3.4,<0.4.0)", "sphinx-pytest", "sphinx (<5.2)"] +python-versions = ">=2.7" [[package]] name = "ncclient" @@ -485,38 +424,33 @@ nornir = ">=3,<4" [[package]] name = "packaging" -version = "21.3" +version = "23.0" description = "Core utilities for Python packages" category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "paramiko" -version = "2.11.0" +version = "3.1.0" description = "SSH2 protocol library" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] -bcrypt = ">=3.1.3" -cryptography = ">=2.5" -pynacl = ">=1.0.1" -six = "*" +bcrypt = ">=3.2" +cryptography = ">=3.3" +pynacl = ">=1.5" [package.extras] -all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] -ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"] +all = ["pyasn1 (>=0.1.7)", "invoke (>=2.0)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"] -invoke = ["invoke (>=1.3)"] +invoke = ["invoke (>=2.0)"] [[package]] name = "pathspec" -version = "0.10.1" +version = "0.11.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -524,7 +458,7 @@ python-versions = ">=3.7" [[package]] name = "pbr" -version = "5.10.0" +version = "5.11.1" description = "Python Build Reasonableness" category = "dev" optional = false @@ -532,15 +466,15 @@ python-versions = ">=2.6" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "3.1.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)", "sphinx (>=6.1.3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest (>=7.2.1)"] [[package]] name = "pluggy" @@ -550,28 +484,20 @@ category = "dev" optional = false python-versions = ">=3.6" -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} - [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" +name = "pockets" +version = "0.9.1" +description = "A collection of helpful Python tools!" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "*" -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[package.dependencies] +six = ">=1.5.2" [[package]] name = "pycparser" @@ -581,31 +507,9 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -[[package]] -name = "pydocstyle" -version = "6.1.1" -description = "Python docstring style checker" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -snowballstemmer = "*" - -[package.extras] -toml = ["toml"] - -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "pygments" -version = "2.13.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false @@ -613,26 +517,6 @@ python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] - -[[package]] -name = "pylint" -version = "2.13.9" -description = "python code static checker" -category = "dev" -optional = false -python-versions = ">=3.6.2" - -[package.dependencies] -astroid = ">=2.11.5,<=2.12.0-dev0" -colorama = {version = "*", markers = "sys_platform == \"win32\""} -dill = ">=0.2" -isort = ">=4.2.5,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} - -[package.extras] testutil = ["gitpython (>3)"] [[package]] @@ -650,20 +534,9 @@ cffi = ">=1.4.1" docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["railroad-diagrams", "jinja2"] - [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -672,12 +545,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -699,7 +571,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytz" -version = "2022.4" +version = "2022.7.1" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -715,7 +587,7 @@ python-versions = ">=3.6" [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "dev" optional = false @@ -723,7 +595,7 @@ python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -731,6 +603,22 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "13.3.2" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.7.0" + +[package.dependencies] +markdown-it-py = ">=2.2.0,<3.0.0" +pygments = ">=2.13.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + [[package]] name = "ruamel.yaml" version = "0.17.21" @@ -748,12 +636,20 @@ jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] [[package]] name = "ruamel.yaml.clib" -version = "0.2.6" +version = "0.2.7" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "ruff" +version = "0.0.256" +description = "An extremely fast Python linter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "six" version = "1.16.0" @@ -780,23 +676,23 @@ python-versions = "*" [[package]] name = "sphinx" -version = "5.2.3" +version = "6.1.3" description = "Python documentation generator" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.dependencies] alabaster = ">=0.7,<0.8" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" +docutils = ">=0.18,<0.20" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" +Pygments = ">=2.13" +requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" @@ -807,12 +703,12 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] -test = ["pytest (>=4.6)", "html5lib", "typed-ast", "cython"] +lint = ["flake8 (>=3.5.0)", "flake8-simplify", "isort", "ruff", "mypy (>=0.990)", "sphinx-lint", "docutils-stubs", "types-requests"] +test = ["pytest (>=4.6)", "html5lib", "cython"] [[package]] name = "sphinx-autoapi" -version = "2.0.0" +version = "2.0.1" description = "Sphinx API documentation generator" category = "dev" optional = false @@ -831,27 +727,20 @@ dotnet = ["sphinxcontrib-dotnetdomain"] docs = ["sphinx-rtd-theme", "sphinx"] [[package]] -name = "sphinx-rtd-theme" -version = "1.0.0" -description = "Read the Docs theme for Sphinx" +name = "sphinx-pdj-theme" +version = "0.2.1" +description = "PdJ for Sphinx" category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" - -[package.dependencies] -docutils = "<0.18" -sphinx = ">=1.6" - -[package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +python-versions = "*" [[package]] name = "sphinxcontrib-applehelp" -version = "1.0.2" -description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +version = "1.0.4" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" [package.extras] lint = ["flake8", "mypy", "docutils-stubs"] @@ -871,11 +760,11 @@ test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" -version = "2.0.0" +version = "2.0.1" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.extras] lint = ["flake8", "mypy", "docutils-stubs"] @@ -892,6 +781,18 @@ python-versions = ">=3.5" [package.extras] test = ["pytest", "flake8", "mypy"] +[[package]] +name = "sphinxcontrib-napoleon" +version = "0.7" +description = "Sphinx \"napoleon\" extension." +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pockets = ">=0.3" +six = ">=1.5.2" + [[package]] name = "sphinxcontrib-qthelp" version = "1.0.3" @@ -918,14 +819,13 @@ test = ["pytest"] [[package]] name = "stevedore" -version = "3.5.0" +version = "5.0.0" description = "Manage dynamic plugins for Python applications" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" [package.dependencies] -importlib-metadata = {version = ">=1.7.0", markers = "python_version < \"3.8\""} pbr = ">=2.0.0,<2.1.0 || >2.1.0" [[package]] @@ -936,17 +836,9 @@ category = "dev" optional = false python-versions = ">=3.7" -[[package]] -name = "typed-ast" -version = "1.5.4" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -962,11 +854,11 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.15" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -975,7 +867,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "wrapt" -version = "1.14.1" +version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false @@ -983,19 +875,19 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "xmltodict" -version = "0.12.0" +version = "0.13.0" description = "Makes working with XML feel like you are working with JSON" -category = "main" +category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.4" [[package]] name = "yamllint" -version = "1.28.0" +version = "1.29.0" description = "A linter for YAML files." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] pathspec = ">=0.5.3" @@ -1003,36 +895,27 @@ pyyaml = "*" [[package]] name = "zipp" -version = "3.8.1" +version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx (>=3.5)", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "furo", "sphinx-lint", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "flake8 (<5)", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "jaraco.functools", "more-itertools", "big-o", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "pytest-flake8"] [metadata] lock-version = "1.1" -python-versions = "^3.7.0" -content-hash = "0f25883a8d89b635324c1589aa2d6767109526e752a459f0cda0fbf3f3be87cf" +python-versions = "^3.8" +content-hash = "c2310cce97e9fd4c1c55c2557116d3684b16634e2614c956036f37b69c864a49" [metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] +alabaster = [] astroid = [] attrs = [] -babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, -] -bandit = [ - {file = "bandit-1.7.4-py3-none-any.whl", hash = "sha256:412d3f259dab4077d0e7f0c11f50f650cc7d10db905d98f6520a95a18049658a"}, - {file = "bandit-1.7.4.tar.gz", hash = "sha256:2d63a8c573417bae338962d4b9b06fbc6080f74ecd955a092849e1e65c717bd2"}, -] +babel = [] +bandit = [] bcrypt = [] black = [] certifi = [] @@ -1107,142 +990,29 @@ click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] +colorama = [] coverage = [] cryptography = [] -dill = [ - {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, - {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, -] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -gitdb = [ - {file = "gitdb-4.0.9-py3-none-any.whl", hash = "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd"}, - {file = "gitdb-4.0.9.tar.gz", hash = "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"}, -] +docutils = [] +exceptiongroup = [] +gitdb = [] gitpython = [] idna = [] imagesize = [] importlib-metadata = [] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] +iniconfig = [] +isort = [] jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, -] +lazy-object-proxy = [] lxml = [] -markdown-it-py = [ - {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, - {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mdit-py-plugins = [] +markdown-it-py = [] +markupsafe = [] mdurl = [] mypy = [] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -myst-parser = [] +mypy-extensions = [] ncclient = [] nornir = [ {file = "nornir-3.3.0-py3-none-any.whl", hash = "sha256:4590d96edb5044e6a9e6f84e15625d32932177a10654040f99e145d73b352479"}, @@ -1252,49 +1022,21 @@ nornir-utils = [ {file = "nornir_utils-0.2.0-py3-none-any.whl", hash = "sha256:b4c430793a74f03affd5ff2d90abc8c67a28c7ff325f48e3a01a9a44ec71b844"}, {file = "nornir_utils-0.2.0.tar.gz", hash = "sha256:4de6aaa35e5c1a98e1c84db84a008b0b1e974dc65d88484f2dcea3e30c95fbc2"}, ] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -paramiko = [ - {file = "paramiko-2.11.0-py2.py3-none-any.whl", hash = "sha256:655f25dc8baf763277b933dfcea101d636581df8d6b9774d1fb653426b72c270"}, - {file = "paramiko-2.11.0.tar.gz", hash = "sha256:003e6bee7c034c21fbb051bf83dc0a9ee4106204dd3c53054c71452cc4ec3938"}, -] +packaging = [] +paramiko = [] pathspec = [] pbr = [] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] +platformdirs = [] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] +pockets = [] pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] pygments = [] -pylint = [ - {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, - {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, -] pynacl = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -1307,10 +1049,6 @@ pynacl = [ {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, ] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] pytest = [] pytest-cov = [] pytz = [] @@ -1349,41 +1087,14 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] +requests = [] +rich = [] "ruamel.yaml" = [ {file = "ruamel.yaml-0.17.21-py3-none-any.whl", hash = "sha256:742b35d3d665023981bd6d16b3d24248ce5df75fdb4e2924e93a05c1f8b61ca7"}, {file = "ruamel.yaml-0.17.21.tar.gz", hash = "sha256:8b7ce697a2f212752a35c1ac414471dc16c424c9573be4926b56ff3f5d23b7af"}, ] -"ruamel.yaml.clib" = [ - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6e7be2c5bcb297f5b82fee9c665eb2eb7001d1050deaba8471842979293a80b0"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:221eca6f35076c6ae472a531afa1c223b9c29377e62936f61bc8e6e8bdc5f9e7"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win32.whl", hash = "sha256:1070ba9dd7f9370d0513d649420c3b362ac2d687fe78c6e888f5b12bf8bc7bee"}, - {file = "ruamel.yaml.clib-0.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:77df077d32921ad46f34816a9a16e6356d8100374579bc35e15bab5d4e9377de"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:cfdb9389d888c5b74af297e51ce357b800dd844898af9d4a547ffc143fa56751"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7b2927e92feb51d830f531de4ccb11b320255ee95e791022555971c466af4527"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win32.whl", hash = "sha256:ada3f400d9923a190ea8b59c8f60680c4ef8a4b0dfae134d2f2ff68429adfab5"}, - {file = "ruamel.yaml.clib-0.2.6-cp35-cp35m-win_amd64.whl", hash = "sha256:de9c6b8a1ba52919ae919f3ae96abb72b994dd0350226e28f3686cb4f142165c"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d67f273097c368265a7b81e152e07fb90ed395df6e552b9fa858c6d2c9f42502"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:72a2b8b2ff0a627496aad76f37a652bcef400fd861721744201ef1b45199ab78"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win32.whl", hash = "sha256:9efef4aab5353387b07f6b22ace0867032b900d8e91674b5d8ea9150db5cae94"}, - {file = "ruamel.yaml.clib-0.2.6-cp36-cp36m-win_amd64.whl", hash = "sha256:846fc8336443106fe23f9b6d6b8c14a53d38cef9a375149d61f99d78782ea468"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0847201b767447fc33b9c235780d3aa90357d20dd6108b92be544427bea197dd"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:78988ed190206672da0f5d50c61afef8f67daa718d614377dcd5e3ed85ab4a99"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win32.whl", hash = "sha256:a49e0161897901d1ac9c4a79984b8410f450565bbad64dbfcbf76152743a0cdb"}, - {file = "ruamel.yaml.clib-0.2.6-cp37-cp37m-win_amd64.whl", hash = "sha256:bf75d28fa071645c529b5474a550a44686821decebdd00e21127ef1fd566eabe"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a32f8d81ea0c6173ab1b3da956869114cae53ba1e9f72374032e33ba3118c233"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7f7ecb53ae6848f959db6ae93bdff1740e651809780822270eab111500842a84"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win32.whl", hash = "sha256:89221ec6d6026f8ae859c09b9718799fea22c0e8da8b766b0b2c9a9ba2db326b"}, - {file = "ruamel.yaml.clib-0.2.6-cp38-cp38-win_amd64.whl", hash = "sha256:31ea73e564a7b5fbbe8188ab8b334393e06d997914a4e184975348f204790277"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc6a613d6c74eef5a14a214d433d06291526145431c3b964f5e16529b1842bed"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1866cf2c284a03b9524a5cc00daca56d80057c5ce3cdc86a52020f4c720856f0"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win32.whl", hash = "sha256:3fb9575a5acd13031c57a62cc7823e5d2ff8bc3835ba4d94b921b4e6ee664104"}, - {file = "ruamel.yaml.clib-0.2.6-cp39-cp39-win_amd64.whl", hash = "sha256:825d5fccef6da42f3c8eccd4281af399f21c02b32d98e113dbc631ea6a6ecbc7"}, - {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, -] +"ruamel.yaml.clib" = [] +ruff = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1398,26 +1109,18 @@ snowballstemmer = [ ] sphinx = [] sphinx-autoapi = [] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] +sphinx-pdj-theme = [] +sphinxcontrib-applehelp = [] sphinxcontrib-devhelp = [ {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, ] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] +sphinxcontrib-htmlhelp = [] sphinxcontrib-jsmath = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] +sphinxcontrib-napoleon = [] sphinxcontrib-qthelp = [ {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, @@ -1426,112 +1129,18 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] -stevedore = [ - {file = "stevedore-3.5.0-py3-none-any.whl", hash = "sha256:a547de73308fd7e90075bb4d301405bebf705292fa90a90fc3bcf9133f58616c"}, - {file = "stevedore-3.5.0.tar.gz", hash = "sha256:f40253887d8712eaa2bb0ea3830374416736dc8ec0e22f5a65092c1174c44335"}, -] +stevedore = [] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] typing-extensions = [] unidecode = [] urllib3 = [] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] +wrapt = [] xmltodict = [ - {file = "xmltodict-0.12.0-py2.py3-none-any.whl", hash = "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051"}, - {file = "xmltodict-0.12.0.tar.gz", hash = "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21"}, + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, ] yamllint = [] zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 2ef4019..16f0524 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [tool.poetry] name = "nornir_netconf" -version = "1.1.0" -description = "Netconf plugin for nornir using ncclient" +version = "2.0.0" +description = "NETCONF plugin for Nornir" authors = ["Hugo Tinoco ", "Patrick Ogenstad "] license = "Apache-2.0" readme = "README.md" repository = "https://github.com/h4ndzdatm0ld/nornir_netconf" keywords = ["nornir", "netconf", "ncclient"] -documentation = "https://nornir-netconf.readthedocs.io" +documentation = "https://h4ndzdatm0ld.github.io/nornir_netconf/" packages = [ { include = "nornir_netconf" }, ] @@ -16,9 +16,8 @@ packages = [ "netconf" = "nornir_netconf.plugins.connections:Netconf" [tool.poetry.dependencies] -python = "^3.7.0" ncclient = "^0.6.9" -xmltodict = "^0.12.0" +python = "^3.8" nornir = {version = "^3.0.0", allow-prereleases = true} @@ -26,20 +25,18 @@ nornir = {version = "^3.0.0", allow-prereleases = true} black = "*" pytest-cov = "*" bandit = "*" -pylint = "*" -flake8 = "*" coverage = "*" yamllint = "*" nornir-utils = "*" isort = "*" -pydocstyle = "*" mypy = "*" Sphinx = "*" -myst-parser = "*" sphinx-autoapi = "*" -sphinx-rtd-theme = "*" +sphinx-pdj-theme = "*" +sphinxcontrib-napoleon = "*" pytest = "*" - +xmltodict = "*" +ruff = "*" [build-system] requires = ["poetry_core>=1.0.0"] @@ -51,7 +48,7 @@ multi_line_output = 3 [tool.black] line-length = 120 -target-version = ['py37'] +target-version = ['py38'] include = '\.pyi?$' exclude = ''' ( @@ -66,6 +63,104 @@ exclude = ''' | buck-out | build | dist + | clab-arista-testing.yml + | clab-files )/ ) ''' + +[tool.ruff] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +select = [ + # Pyflakes + "F", + # Pycodestyle + "E", + "W", + # isort + "I001", + # Pylint + "PL", +] +ignore = ["PLR0913"] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +fixable = ["A", "B", "D", "E", "F"] +unfixable = [] + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +per-file-ignores = {} + +# Same as Black. +line-length = 120 + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +target-version = "py38" + +[tool.ruff.mccabe] +# Unlike Flake8, default to a complexity level of 10. +max-complexity = 10 + +[tool.ruff.pydocstyle] +convention = "google" + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +addopts = "-p no:warnings" + +[tool.mypy] +namespace_packages = true +explicit_package_bases = true +show_error_codes = true +enable_error_code = [ + "ignore-without-code", + "truthy-bool", +] +check_untyped_defs = true +ignore_errors = false +ignore_missing_imports = true +strict_optional = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unused_configs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +disallow_any_generics = true +warn_return_any = true +python_version = 3.8 +disallow_subclassing_any = true +no_implicit_optional = true +implicit_reexport = true +strict_equality = true +exclude = "tests/" + +[tool.bandit] +exclude_dirs = ["tests"] diff --git a/tests/conftest.py b/tests/conftest.py index 7f83778..294fe40 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,29 +1,44 @@ """Conftest for nornir_netconf UnitTests.""" import os import shutil +import time +from distutils.util import strtobool +from typing import Any, Dict, List import pytest +import xmltodict from nornir import InitNornir from nornir.core.state import GlobalState +from nornir.core.task import Result +from nornir_utils.plugins.functions import print_result + + +def is_truthy(value: str) -> bool: + """Evaluate arg and determine truthy value.""" + if isinstance(value, bool): + return value + return bool(strtobool(str(value))) + + +SKIP_INTEGRATION_TESTS = is_truthy(os.environ.get("SKIP_INTEGRATION_TESTS", True)) -# pytest mark decorator to skip integration tests if INTEGRATION_TESTS=True -# These tests will connect to local lab environment to validate actual responses -# from locallly hosted network devices. skip_integration_tests = pytest.mark.skipif( - os.environ.get("SKIP_INTEGRATION_TESTS", True), reason="Do not run integration tests" + SKIP_INTEGRATION_TESTS, + reason="Integration tests require virtual devices running.", ) global_data = GlobalState(dry_run=True) DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +CONFIGS_DIR = f"{DIR_PATH}/test_data/configs" # If NORNIR_LOG set to True, the log won't be deleted in teardown. nornir_logfile = os.environ.get("NORNIR_LOG", False) -@pytest.fixture(scope="module", autouse=True) +@pytest.fixture() def nornir(): """Initializes nornir""" - nr_nr = InitNornir( + nornir = InitNornir( inventory={ "plugin": "SimpleInventory", "options": { @@ -35,14 +50,14 @@ def nornir(): logging={"log_file": f"{DIR_PATH}/test_data/nornir_test.log", "level": "DEBUG"}, dry_run=True, ) - nr_nr.data = global_data - return nr_nr + nornir.data = global_data + return nornir @pytest.fixture(scope="session", autouse=True) def schema_path(): """Schema path, test data.""" - return "tests/test_data/schema_path" + return f"{DIR_PATH}/test_data/schemas" @pytest.fixture(scope="session", autouse=True) @@ -73,32 +88,6 @@ def reset_data(): global_data.reset_failed_hosts() -class FakeRpcObject: - """Test Class.""" - - def __init__(self): - self.ok = False - self.data_xml = False - self.error = "" - self.errors = "" - self.xml = False - - def set_ok(self, set: bool): - """Set ok.""" - if set: - self.ok = True - - def set_data_xml(self, set: bool): - """Set data_xml.""" - if set: - self.data_xml = True - - def set_xml(self, set: bool): - """Set xml.""" - if set: - self.xml = True - - # PAYLOADS @@ -120,16 +109,41 @@ def sros_config_payload(): """ -@pytest.fixture(scope="function", autouse=True) -def iosxr_config_payload(): - return """ - - - 80 - true - - 200 - - - - """ +def xml_dict(xml: str) -> Dict[str, Any]: + """Convert XML to Dict. + + Args: + xml (str): XML string + + Returns: + Dict: XML converted to Dict + """ + return xmltodict.parse(str(xml)) + + +def eval_multi_task_result(hosts: List, result: Result) -> None: + """Repeatable multi host common test operation when running multi tasks.""" + print_result(result) + assert set(hosts) == set(list(result.keys())) + for host in hosts: + for task in range(len(result[host])): + assert not result[host][task].failed + + +def eval_multi_result(hosts: List, result: Result) -> None: + """Repeatable multi host common test operation.""" + print_result(result) + assert set(hosts) == set(list(result.keys())) + for host in hosts: + if hasattr(result[host].result.rpc, "ok"): + assert result[host].result.rpc.ok + assert not result[host].failed + + +@pytest.fixture(autouse=True, scope="module") +def slow_down_tests(): + yield + if SKIP_INTEGRATION_TESTS: + return + else: + time.sleep(3) diff --git a/tests/integration/arista/test_arista_connection.py b/tests/integration/arista/test_arista_connection.py new file mode 100644 index 0000000..b7d8b56 --- /dev/null +++ b/tests/integration/arista/test_arista_connection.py @@ -0,0 +1,48 @@ +"""Test NETCONF Connection.""" +import os + +from ncclient.capabilities import Capabilities + +from nornir_netconf.plugins.helpers.models import RpcResult +from nornir_netconf.plugins.tasks import netconf_capabilities + +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +DEVICE_NAME = "ceos" +CAP = "http://openconfig.net/yang/policy-forwarding?module=openconfig-policy-forwarding&revision=2021-08-06" + + +def test_netconf_connection_missing_ssh_keyfile(nornir): + """Test netconf connection - no ssh config file.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_capabilities) + + assert isinstance(result[DEVICE_NAME].result, RpcResult) + assert isinstance(result[DEVICE_NAME].result.rpc, Capabilities) + + +def test_netconf_connection_non_existent_ssh_config(nornir): + """Test netconf connection - bad ssh config path.""" + nr = nornir.filter(name=DEVICE_NAME) + nr.config.ssh.config_file = "i dont exist" + result = nr.run(netconf_capabilities) + assert nr.config.ssh.config_file == "i dont exist" + assert isinstance(result[DEVICE_NAME].result, RpcResult) + assert CAP in result[DEVICE_NAME].result.rpc + + +def test_netconf_connection_ssh_config_exists(nornir): + nr = nornir.filter(name=DEVICE_NAME) + nr.config.ssh.config_file = f"{DIR_PATH}/inventory_data/ssh_config" + result = nr.run(netconf_capabilities) + + assert isinstance(result[DEVICE_NAME].result, RpcResult) + assert CAP in [cap for cap in result[DEVICE_NAME].result.rpc] + + +def test_netconf_connection_ssh_keyfile(nornir): + """Test netconf connection - with shh config file.""" + device_name = "ceos_empty_ssh_file" + nr = nornir.filter(name=device_name) + result = nr.run(netconf_capabilities) + assert isinstance(result[device_name].result, RpcResult) + assert isinstance(result[device_name].result.rpc, Capabilities) diff --git a/tests/integration/arista/test_arista_edit_config.py b/tests/integration/arista/test_arista_edit_config.py new file mode 100644 index 0000000..32a76d9 --- /dev/null +++ b/tests/integration/arista/test_arista_edit_config.py @@ -0,0 +1,43 @@ +"""Test Edit Config on Arista.""" +from random import randint +from string import Template + +from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import ( + netconf_commit, + netconf_edit_config, + netconf_get_config, +) +from tests.conftest import xml_dict + +DEVICE_NAME = "ceos" + +BFD_STATE = str(bool(randint(0, 1))).lower() +CONFIG_TEMPLATE = """ + + + + ${bfd_state} + + + + """ +CONFIG = Template(CONFIG_TEMPLATE).substitute(bfd_state=BFD_STATE) + + +def test_edit_ceos_config(nornir): + """Edit Config and then pull config to validate the change.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(task=netconf_edit_config, config=CONFIG, target="candidate") + print_result(result) + result = nr.run(task=netconf_commit) + print_result(result) + # Pull config and assert the default 'enabled' is set to dynamic variable `BFD_STATE` + result = nr.run( + netconf_get_config, + source="running", + ) + assert result[DEVICE_NAME].result.rpc + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert BFD_STATE == parsed["data"]["bfd"]["config"]["enabled"] diff --git a/tests/integration/arista/test_arista_get.py b/tests/integration/arista/test_arista_get.py new file mode 100644 index 0000000..d6883c0 --- /dev/null +++ b/tests/integration/arista/test_arista_get.py @@ -0,0 +1,28 @@ +"""Test NETCONF get.""" +from nornir_netconf.plugins.tasks import netconf_get +from tests.conftest import xml_dict + +DEVICE_NAME = "ceos" + + +def test_netconf_get(nornir): + """Test NETCONF get operation.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_get) + parsed = xml_dict(result[DEVICE_NAME].result.rpc) + assert result[DEVICE_NAME].result.rpc.ok + assert parsed["rpc-reply"]["data"]["system"]["config"]["hostname"] == "ceos" + + +def test_netconf_get_subtree(nornir): + """Test NETCONF get with subtree. + + Subtree filter is used to get specific data from the device which returns a smaller RPC Reply. + """ + nr = nornir.filter(name=DEVICE_NAME) + + path = "" + result = nr.run(netconf_get, path=path, filter_type="subtree") + parsed = xml_dict(result[DEVICE_NAME].result.rpc) + + assert parsed["rpc-reply"]["data"]["acl"]["state"]["counter-capability"] == "AGGREGATE_ONLY" diff --git a/tests/integration/arista/test_arista_get_config.py b/tests/integration/arista/test_arista_get_config.py new file mode 100644 index 0000000..f15f37d --- /dev/null +++ b/tests/integration/arista/test_arista_get_config.py @@ -0,0 +1,35 @@ +from nornir_netconf.plugins.tasks import netconf_get_config +from tests.conftest import xml_dict + +DEVICE_NAME = "ceos" + + +def test_netconf_get_config_running(nornir): + """Test get running config as default.""" + nr = nornir.filter(name=DEVICE_NAME) + + result = nr.run(netconf_get_config, source="running") + assert result[DEVICE_NAME].result.rpc.ok + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert parsed["data"]["system"]["config"]["hostname"] == "ceos" + + +def test_netconf_get_config_subtree(nornir): + """Test filter subtree of get_config.""" + nr = nornir.filter(name=DEVICE_NAME) + eth3 = """ + + + Management1 + + + """ + result = nr.run( + netconf_get_config, + source="running", + path=eth3, + filter_type="subtree", + ) + assert result[DEVICE_NAME].result.rpc.ok + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert parsed["data"]["interfaces"]["interface"]["name"] == "Management1" diff --git a/tests/integration/arista/test_arista_lock.py b/tests/integration/arista/test_arista_lock.py new file mode 100644 index 0000000..a25d06c --- /dev/null +++ b/tests/integration/arista/test_arista_lock.py @@ -0,0 +1,26 @@ +"""Test NETCONF lock - integration.""" +from ncclient.manager import Manager +from ncclient.operations.rpc import RPCReply + +from nornir_netconf.plugins.helpers import RpcResult +from nornir_netconf.plugins.tasks import netconf_lock + +DEVICE_NAME = "ceos" + + +def test_netconf_lock(nornir): + """Test Netconf Lock.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_lock, datastore="running", operation="lock") + assert result[DEVICE_NAME].result.rpc.ok + assert isinstance(result[DEVICE_NAME].result, RpcResult) + assert isinstance(result[DEVICE_NAME].result.manager, Manager) + assert isinstance(result[DEVICE_NAME].result.rpc, RPCReply) + + +def test_netconf_lock_failed(nornir): + """Test Netconf Lock - failed.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_lock, datastore="running", operation="lock") + assert result[DEVICE_NAME].failed + result = nr.run(netconf_lock, datastore="running", operation="unlock") diff --git a/tests/integration/cisco_iosxr/test_iosxr_integration.py b/tests/integration/cisco_iosxr/test_iosxr_integration.py deleted file mode 100644 index 29eacd3..0000000 --- a/tests/integration/cisco_iosxr/test_iosxr_integration.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Integration test against IOSXR device.""" -from nornir_utils.plugins.functions import print_result - -from nornir_netconf.plugins.tasks import ( - netconf_capabilities, - netconf_commit, - netconf_edit_config, - netconf_get, - netconf_get_config, - netconf_lock, -) -from tests.conftest import skip_integration_tests - -DEVICE_NAME = "iosxr_rtr" - - -@skip_integration_tests -def test_iosxr_netconf_capabilities(nornir): - """Test NETCONF Capabilities.""" - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_capabilities) - assert any(cap for cap in result[DEVICE_NAME].result if "http://cisco.com/ns/yang/cisco-xr-ietf" in cap) - - -@skip_integration_tests -def test_iosxr_netconf_get_config(nornir): - """Test NETCONF get config.""" - nr = nornir.filter(name=DEVICE_NAME) - - result = nr.run( - netconf_get_config, - source="running", - path=""" - - - """, - filter_type="subtree", - xmldict=True, - ) - assert "MgmtEth0/0/CPU0/0" == result[DEVICE_NAME].result["xml_dict"]["data"]["interfaces"]["interface"][0]["name"] - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["rpc"].data_xml - # with open("tests/test_data/get-iosxr-config.xml", "w+") as file: - # file.write(result[DEVICE_NAME].result["rpc"].data_xml) - - -@skip_integration_tests -def test_iosxr_netconf_get(nornir): - """Test NETCONF get operation.""" - nr = nornir.filter(name=DEVICE_NAME) - filter = """ - - - MgmtEth0/0/CPU0/0 - - - """ - result = nr.run(netconf_get, filter_type="subtree", path=filter, xmldict=True) - assert result[DEVICE_NAME].result - assert result[DEVICE_NAME].result["rpc"].data_xml - assert result[DEVICE_NAME].result["xml_dict"]["data"]["interfaces"]["interface"]["config"]["enabled"] - - -@skip_integration_tests -def test_sros_netconf_lock_operations(nornir, iosxr_config_payload): - """Test NETCONF Lock, extract manager and use it to edit-config. - - Afterwards, use netconf_lock with unlock operations to unlock. - """ - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_lock, datastore="candidate", operation="lock") - manager = result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["data_xml"] - # Extract manager from lock operation. - manager = result[DEVICE_NAME].result["manager"] - # print_result(result) - - # Edit Config - result = nr.run(netconf_edit_config, config=iosxr_config_payload, target="candidate", xmldict=True, manager=manager) - # print_result(result) - assert not result[DEVICE_NAME].result["error"] - assert not result[DEVICE_NAME].result["errors"] - assert result[DEVICE_NAME].result["ok"] - - # Commit Config - result = nr.run(netconf_commit, manager=manager, xmldict=True) - # print_result(result) - assert not result[DEVICE_NAME].result["error"] - assert not result[DEVICE_NAME].result["errors"] - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() - assert result[DEVICE_NAME].result["ok"] - - # Unlock candidate datastore. - result = nr.run(netconf_lock, datastore="candidate", operation="unlock", manager=manager) - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["data_xml"] - assert result[DEVICE_NAME].result["ok"] - # print_result(result) - - -@skip_integration_tests -def test_iosxr_netconf_edit_config(nornir, iosxr_config_payload): - """Test NETCONF edit-config - Post Lock / Unlock operations.""" - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_edit_config, config=iosxr_config_payload, target="candidate", xmldict=True) - assert not result[DEVICE_NAME].result["errors"] - assert result[DEVICE_NAME].result["ok"] - - # print_result(result) - - # Commit Config - result = nr.run(netconf_commit, xmldict=True) - assert result[DEVICE_NAME].result["ok"] - print_result(result) diff --git a/tests/integration/common/test_capabilities.py b/tests/integration/common/test_capabilities.py new file mode 100644 index 0000000..1fe3712 --- /dev/null +++ b/tests/integration/common/test_capabilities.py @@ -0,0 +1,39 @@ +"""Test NETCONF capabilities.""" + +from typing import Dict + +from nornir.core.filter import F + +from nornir_netconf.plugins.tasks import netconf_capabilities +from tests.conftest import eval_multi_task_result, skip_integration_tests + +CEOS_EXPECTED_CAPABILITY = ( + "http://openconfig.net/yang/policy-forwarding?module=openconfig-policy-forwarding&revision=2021-08-06" +) +IOSXR_EXPECTED_CAPABILITY = ( + "http://cisco.com/ns/yang/Cisco-IOS-XR-es-acl-datatypes?module=Cisco-IOS-XR-es-acl-datatypes&revision=2015-11-09" +) +IOSXE_EXPECTED_CAPABILITY = ( + "http://cisco.com/ns/yang/Cisco-IOS-XE-device-tracking?module=Cisco-IOS-XE-device-tracking&revision=2020-03-01" +) +SROS_EXPECTED_CAPABILITY = "urn:nokia.com:sros:ns:yang:sr:types-rsvp?module=nokia-types-rsvp&revision=2018-02-08" + + +CAPABILITIES: Dict = { + "ceos": CEOS_EXPECTED_CAPABILITY, + "iosxe_rtr": IOSXE_EXPECTED_CAPABILITY, + "nokia_rtr": SROS_EXPECTED_CAPABILITY, + "iosxr_rtr": IOSXR_EXPECTED_CAPABILITY, +} + + +@skip_integration_tests +def test_netconf_capabilities(nornir, schema_path): + """Test NETCONF Capabilities.""" + nr = nornir.filter(F(groups__contains="integration")) + hosts = list(nr.inventory.hosts.keys()) + result = nr.run(netconf_capabilities) + eval_multi_task_result(hosts=hosts, result=result) + for host in hosts: + capabilities = [cap for cap in result[host][0].result.rpc] + assert CAPABILITIES[host] in capabilities diff --git a/tests/integration/common/test_lock_operations.py b/tests/integration/common/test_lock_operations.py new file mode 100644 index 0000000..2da6e59 --- /dev/null +++ b/tests/integration/common/test_lock_operations.py @@ -0,0 +1,69 @@ +"""Test NETCONF lock - integration.""" +import pytest + +from nornir_netconf.plugins.tasks import netconf_lock +from tests.conftest import ( + eval_multi_result, + eval_multi_task_result, + skip_integration_tests, +) + +GROUP_NAME = "integration" + + +@skip_integration_tests +@pytest.mark.parametrize( + "datastore, expected_hosts", [("running", ["ceos", "iosxe_rtr"]), ("candidate", ["iosxr_rtr", "nokia_rtr"])] +) +def test_netconf_lock_and_unlock_datastore(nornir, datastore, expected_hosts): + """Test Netconf Lock and Unlock with manager carrying.""" + nr = nornir.filter(lock_datastore=datastore) + result = nr.run(netconf_lock, datastore=datastore, operation="lock") + eval_multi_result(expected_hosts, result) + result = nr.run(netconf_lock, datastore=datastore, operation="unlock") + assert set(expected_hosts) == set(list(result.keys())) + eval_multi_result(expected_hosts, result) + + +def global_lock(task, datastore: str, operation: str): + """Test global lock operation of 'running' datastore.""" + if operation == "unlock": + manager = task.host["manager"] + print(manager) + else: + manager = None + result = task.run(netconf_lock, datastore=datastore, operation=operation, manager=manager) + task.host["manager"] = result.result.manager + if hasattr(result.result.rpc, "ok"): + assert result.result.rpc.ok + assert not result.failed + + +@skip_integration_tests +@pytest.mark.parametrize( + "datastore, expected_hosts", [("running", ["ceos", "iosxe_rtr"]), ("candidate", ["iosxr_rtr", "nokia_rtr"])] +) +def test_netconf_global_lock(datastore, expected_hosts, nornir): + """Test Netconf Lock and Unlock with carried manager session.""" + nr = nornir.filter(lock_datastore=datastore) + result = nr.run(global_lock, datastore=datastore, operation="lock") + eval_multi_task_result(expected_hosts, result) + result = nr.run(global_lock, datastore=datastore, operation="unlock") + eval_multi_task_result(expected_hosts, result) + + +@skip_integration_tests +@pytest.mark.parametrize( + "datastore, expected_hosts", [("running", ["ceos", "iosxe_rtr"]), ("candidate", ["iosxr_rtr", "nokia_rtr"])] +) +def test_netconf_lock_lock_failed(datastore, expected_hosts, nornir): + """Test Netconf Lock and attempting second lock - failed.""" + nr = nornir.filter(lock_datastore=datastore) + result = nr.run(global_lock, datastore=datastore, operation="lock") + eval_multi_task_result(expected_hosts, result) + result = nr.run(global_lock, datastore=datastore, operation="lock") + assert set(expected_hosts) == set(list(result.keys())) + for host in expected_hosts: + for task in range(len(result[host])): + assert result[host][task].failed + result = nr.run(global_lock, datastore=datastore, operation="unlock") diff --git a/tests/integration/common/test_schemas.py b/tests/integration/common/test_schemas.py new file mode 100644 index 0000000..172f4c4 --- /dev/null +++ b/tests/integration/common/test_schemas.py @@ -0,0 +1,47 @@ +"""Test Get Schemas from all vendors.""" +from typing import Dict + +from nornir.core.filter import F + +from nornir_netconf.plugins.tasks import netconf_get, netconf_get_schemas +from tests.conftest import skip_integration_tests, xml_dict + +# from nornir_utils.plugins.functions import print_result + + +@skip_integration_tests +def test_netconf_capabilities_get_schema(nornir, schema_path): + """Test NETCONF Capabilities + Get Schemas success.""" + nr = nornir.filter(F(groups__contains="integration")) + hosts = list(nr.inventory.hosts.keys()) + schema_map: Dict[str, str] = {} + + filter = """ + + + + + """ + result = nr.run(netconf_get, path=filter, filter_type="subtree") + for host in hosts: + assert not result[host][0].failed + parsed = xml_dict(result[host][0].result.rpc.data_xml) + if "rpc-reply" in list(parsed.keys()): + first_schema = parsed["rpc-reply"]["data"]["netconf-state"]["schemas"]["schema"][0] + + else: + first_schema = parsed["data"]["netconf-state"]["schemas"]["schema"][0] + schema_map.setdefault(host, first_schema["identifier"]) + # example = { + # "identifier": "iana-if-type", + # "version": "2014-05-08", + # "format": "yang", + # "namespace": "urn:ietf:params:xml:ns:yang:iana-if-type", + # "location": "NETCONF", + # } + for host in hosts: + nr = nornir.filter(name=host) + schema = nr.run(netconf_get_schemas, schemas=[schema_map[host]], schema_path=schema_path) + assert schema[host].result.files + assert not schema[host].result.errors + assert schema[host].result.directory diff --git a/tests/integration/iosxe/test_iosxe_edit_config.py b/tests/integration/iosxe/test_iosxe_edit_config.py new file mode 100644 index 0000000..65bd8b6 --- /dev/null +++ b/tests/integration/iosxe/test_iosxe_edit_config.py @@ -0,0 +1,45 @@ +"""Integration test configuration edits against IOSXE device.""" +from random import randint +from string import Template + +from nornir_netconf.plugins.tasks import netconf_edit_config, netconf_get_config +from tests.conftest import skip_integration_tests, xml_dict + +# from nornir_utils.plugins.functions import print_result + + +DEVICE_NAME = "iosxe_rtr" + +RANDOM_DESCRIPTION = f"NORNIR-NETCONF-DESCRIPTION-{randint(0, 100)}" +CONFIG_TEMPLATE = """ + + + + GigabitEthernet1 + + GigabitEthernet1 + ${random_description} + + + + + """ +CONFIG = Template(CONFIG_TEMPLATE).substitute(random_description=RANDOM_DESCRIPTION) + + +@skip_integration_tests +def test_netconf_edit_config(nornir): + """Test Edit Config.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(task=netconf_edit_config, config=CONFIG, target="running") + assert result[DEVICE_NAME].result.rpc.ok + + # Validate config change is in running config datastore + result = nr.run( + netconf_get_config, + source="running", + ) + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert RANDOM_DESCRIPTION == parsed["data"]["interfaces"][0]["interface"]["description"] diff --git a/tests/integration/iosxe/test_iosxe_get.py b/tests/integration/iosxe/test_iosxe_get.py new file mode 100644 index 0000000..44b841b --- /dev/null +++ b/tests/integration/iosxe/test_iosxe_get.py @@ -0,0 +1,25 @@ +"""Integration test against IOSXE device.""" +# from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import netconf_get +from tests.conftest import skip_integration_tests, xml_dict + +DEVICE_NAME = "iosxe_rtr" + + +@skip_integration_tests +def test_iosxe_netconf_get(nornir): + """Test NETCONF get operation.""" + nr = nornir.filter(name=DEVICE_NAME) + filter = """ + + + + + + """ + result = nr.run(netconf_get, filter_type="subtree", path=filter) + parsed = xml_dict(result[DEVICE_NAME].result.rpc) + + assert result[DEVICE_NAME].result + assert parsed["rpc-reply"]["data"]["native"]["ip"]["domain"]["name"] == "example.com" diff --git a/tests/integration/iosxe/test_iosxe_get_config.py b/tests/integration/iosxe/test_iosxe_get_config.py new file mode 100644 index 0000000..a836416 --- /dev/null +++ b/tests/integration/iosxe/test_iosxe_get_config.py @@ -0,0 +1,51 @@ +"""Integration test against IOSXE device.""" +# from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import netconf_get_config +from tests.conftest import CONFIGS_DIR, skip_integration_tests, xml_dict + +DEVICE_NAME = "iosxe_rtr" + + +@skip_integration_tests +def test_iosxe_netconf_get_config(nornir): + """Test NETCONF get config.""" + nr = nornir.filter(name=DEVICE_NAME) + + result = nr.run( + netconf_get_config, + source="running", + path=""" + + + GigabitEthernet1 + + + """, + filter_type="subtree", + ) + + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "10.0.0.15" == parsed["data"]["interfaces"]["interface"]["ipv4"]["address"]["ip"] + + with open(f"{CONFIGS_DIR}/iosxe-interface-gigabitethernet1.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) + + +@skip_integration_tests +def test_iosxe_netconf_get_full_config(nornir): + """Test NETCONF get full config.""" + nr = nornir.filter(name=DEVICE_NAME) + + result = nr.run( + netconf_get_config, + source="running", + ) + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "vr-csr-1" == parsed["data"]["native"]["hostname"] + with open(f"{CONFIGS_DIR}/iosxe-full-config.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) diff --git a/tests/integration/iosxe/test_iosxe_integration.py b/tests/integration/iosxe/test_iosxe_integration.py deleted file mode 100644 index c7bd18f..0000000 --- a/tests/integration/iosxe/test_iosxe_integration.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Integration test against IOSXR device.""" -# from nornir_utils.plugins.functions import print_result -from nornir_netconf.plugins.tasks import ( - netconf_capabilities, - netconf_get, - netconf_get_config, -) -from tests.conftest import skip_integration_tests - -DEVICE_NAME = "iosxe_rtr" - - -@skip_integration_tests -def test_iosxe_netconf_capabilities(nornir): - """Test NETCONF Capabilities.""" - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_capabilities) - assert any(cap for cap in result[DEVICE_NAME].result if "cisco-xe-openconfig-acl-deviation" in cap) - - -@skip_integration_tests -def test_iosxe_netconf_get_config(nornir): - """Test NETCONF get config.""" - nr = nornir.filter(name=DEVICE_NAME) - - result = nr.run( - netconf_get_config, - source="running", - path=""" - - - - - - - """, - filter_type="subtree", - xmldict=True, - ) - # assert ( - # "Welcome to the DevNet Sandbox" - # in result[DEVICE_NAME].result["xml_dict"]["data"]["native"]["banner"]["motd"]["banner"] - # ) - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["rpc"].data_xml - # with open("tests/test_data/get-iosxe-config-filter.xml", "w+") as file: - # file.write(result[DEVICE_NAME].result["rpc"].data_xml) - - -@skip_integration_tests -def test_iosxe_netconf_get(nornir): - """Test NETCONF get operation.""" - nr = nornir.filter(name=DEVICE_NAME) - filter = """ - - - - - - """ - result = nr.run(netconf_get, filter_type="subtree", path=filter, xmldict=True) - assert result[DEVICE_NAME].result - assert result[DEVICE_NAME].result["xml_dict"]["data"]["native"]["ip"]["domain"]["name"] == "lab.devnetsandbox.local" diff --git a/tests/integration/iosxr/test_iosxr_edit_config.py b/tests/integration/iosxr/test_iosxr_edit_config.py new file mode 100644 index 0000000..db808c4 --- /dev/null +++ b/tests/integration/iosxr/test_iosxr_edit_config.py @@ -0,0 +1,52 @@ +"""Integration test against IOSXR device.""" +from random import randint +from string import Template + +from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import ( + netconf_commit, + netconf_edit_config, + netconf_get_config, +) +from tests.conftest import skip_integration_tests, xml_dict + +DEVICE_NAME = "iosxr_rtr" +RANDOM_TIMER = randint(10, 100) +CONFIG_TEMPLATE = """ + + + ${timer} + true + + 200 + + + +""" +CONFIG = Template(CONFIG_TEMPLATE).substitute(timer=RANDOM_TIMER) + + +@skip_integration_tests +def test_iosxr_netconf_edit_config(nornir): + """Test NETCONF edit-config from candidate datastore and commit.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_edit_config, config=CONFIG, target="candidate") + assert result[DEVICE_NAME].result.rpc.ok + print_result(result) + + # Commit Config + result = nr.run(netconf_commit) + assert result[DEVICE_NAME].result.rpc.ok + print_result(result) + + result = nr.run( + netconf_get_config, + source="running", + ) + print_result(result) + + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert str(RANDOM_TIMER) == parsed["data"]["cdp"]["timer"] diff --git a/tests/integration/iosxr/test_iosxr_get.py b/tests/integration/iosxr/test_iosxr_get.py new file mode 100644 index 0000000..5824097 --- /dev/null +++ b/tests/integration/iosxr/test_iosxr_get.py @@ -0,0 +1,23 @@ +"""Integration test against IOSXR device.""" +from nornir_netconf.plugins.tasks import netconf_get +from tests.conftest import skip_integration_tests, xml_dict + +DEVICE_NAME = "iosxr_rtr" + + +@skip_integration_tests +def test_iosxr_netconf_get(nornir): + """Test NETCONF get operation.""" + nr = nornir.filter(name=DEVICE_NAME) + filter = """ + + + MgmtEth0/0/CPU0/0 + + + """ + result = nr.run(netconf_get, filter_type="subtree", path=filter) + assert result[DEVICE_NAME].result + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "true" == parsed["data"]["interfaces"]["interface"]["state"]["enabled"] diff --git a/tests/integration/iosxr/test_iosxr_get_config.py b/tests/integration/iosxr/test_iosxr_get_config.py new file mode 100644 index 0000000..ce7a08c --- /dev/null +++ b/tests/integration/iosxr/test_iosxr_get_config.py @@ -0,0 +1,28 @@ +"""Integration test against IOSXR device.""" +from nornir_netconf.plugins.tasks import netconf_get_config +from tests.conftest import CONFIGS_DIR, skip_integration_tests, xml_dict + +DEVICE_NAME = "iosxr_rtr" + + +@skip_integration_tests +def test_iosxr_netconf_get_config(nornir): + """Test NETCONF get config.""" + nr = nornir.filter(name=DEVICE_NAME) + + result = nr.run( + netconf_get_config, + source="running", + path=""" + + + """, + filter_type="subtree", + ) + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "MgmtEth0/0/CPU0/0" == parsed["data"]["interfaces"]["interface"][0]["name"] + + with open(f"{CONFIGS_DIR}/iosxr-interfaces.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) diff --git a/tests/integration/netopeer2/test_netconf_capabilities.py b/tests/integration/netopeer2/test_netconf_capabilities.py deleted file mode 100644 index 8485305..0000000 --- a/tests/integration/netopeer2/test_netconf_capabilities.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Test NETCONF capabilities.""" -from nornir_netconf.plugins.tasks import netconf_capabilities - -# from nornir_utils.plugins.functions import print_result - - -def test_netconf_capabilities(nornir): - """Test NETCONF Capabilities.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_capabilities) - # print_result(result) - assert result.items() - for _, v in result.items(): - assert "urn:ietf:params:netconf:capability:writable-running:1.0" in v.result diff --git a/tests/integration/netopeer2/test_netconf_connection.py b/tests/integration/netopeer2/test_netconf_connection.py deleted file mode 100644 index 9ec604f..0000000 --- a/tests/integration/netopeer2/test_netconf_connection.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Test NETCONF Connection.""" -import os - -from nornir_netconf.plugins.tasks import netconf_capabilities - -DIR_PATH = os.path.dirname(os.path.realpath(__file__)) - - -def test_netconf_connection_missing_ssh_keyfile(nornir): - """Test netconf connection - no ssh config file.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_capabilities) - assert isinstance(result["netconf_sysrepo"].result, list) - - -def test_netconf_connection_non_existent_ssh_config(nornir): - """Test netconf connection - bad ssh config path.""" - nr = nornir.filter(name="netconf_sysrepo") - - nr.config.ssh.config_file = "i dont exist" - result = nr.run(netconf_capabilities) - - assert nr.config.ssh.config_file == "i dont exist" - assert isinstance(result["netconf_sysrepo"].result, list) - assert "urn:ietf:params:netconf:capability:candidate:1.0" in result["netconf_sysrepo"].result - - -def test_netconf_connection_ssh_config_exists(nornir): - nr = nornir.filter(name="netconf_sysrepo") - - nr.config.ssh.config_file = f"{DIR_PATH}/inventory_data/ssh_config" - result = nr.run(netconf_capabilities) - - assert isinstance(result["netconf_sysrepo"].result, list) - assert "urn:ietf:params:netconf:capability:candidate:1.0" in result["netconf_sysrepo"].result - - -def test_netconf_connection_ssh_keyfile(nornir): - """Test netconf connection - with shh config file.""" - nr = nornir.filter(name="netconf3") - result = nr.run(netconf_capabilities) - assert isinstance(result["netconf3"].result, list) - - -def test_netconf_connection_ssh_keyfile_null(nornir): - """Test netconf connection - with null shh config file.""" - nr = nornir.filter(name="netconf4") - result = nr.run(netconf_capabilities) - assert isinstance(result["netconf4"].result, list) diff --git a/tests/integration/netopeer2/test_netconf_edit_config.py b/tests/integration/netopeer2/test_netconf_edit_config.py deleted file mode 100644 index d1fa8e9..0000000 --- a/tests/integration/netopeer2/test_netconf_edit_config.py +++ /dev/null @@ -1,42 +0,0 @@ -# from nornir_netconf.plugins.tasks import netconf_edit_config, netconf_get_config -# from nornir_utils.plugins.functions import print_result - -# CONFIG = """ -# -# -# -# -# default-ssh-updated -# -# -# -# -# """ - - -# def test_netconf_edit_config(nornir): -# nr = nornir.filter(name="netconf_sysrepo") -# assert nr.inventory.hosts - -# result = nr.run(netconf_get_config) - -# for _, v in result.items(): -# assert "nornir" not in v.result - -# result = nr.run(netconf_edit_config, config=CONFIG, target="running") -# print_result(result) -# assert not result.failed -# assert "" in result["netconf_sysrepo"].result - -# result = nr.run(netconf_get_config, source="candidate") - -# for _, v in result.items(): -# assert "nornir" in v.result - -# status = nr.run(netconf_edit_config, config=CONFIG.format(operation="delete"), target="candidate",) -# assert not status.failed - -# result = nr.run(netconf_get_config, source="candidate") - -# for _, v in result.items(): -# assert "nornir" not in v.result diff --git a/tests/integration/netopeer2/test_netconf_get.py b/tests/integration/netopeer2/test_netconf_get.py deleted file mode 100644 index 84eaec0..0000000 --- a/tests/integration/netopeer2/test_netconf_get.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Test NETCONF get.""" -from nornir_netconf.plugins.tasks import netconf_get - - -def test_netconf_get(nornir): - """Test NETCONF get operation.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_get, xmldict=True) - - assert result["netconf_sysrepo"].result["ok"] - assert ( - "netconf-start-time" - in result["netconf_sysrepo"].result["xml_dict"]["data"]["netconf-state"]["statistics"].keys() - ) - - -def test_netconf_get_subtree(nornir): - """Test NETCONF get with subtree.""" - nr = nornir.filter(name="netconf_sysrepo") - - result = nr.run( - netconf_get, path="", filter_type="subtree", xmldict=True - ) - assert result["netconf_sysrepo"].result["ok"] - assert "netconf-server" in result["netconf_sysrepo"].result["xml_dict"]["data"].keys() diff --git a/tests/integration/netopeer2/test_netconf_get_config.py b/tests/integration/netopeer2/test_netconf_get_config.py deleted file mode 100644 index 8abbb79..0000000 --- a/tests/integration/netopeer2/test_netconf_get_config.py +++ /dev/null @@ -1,42 +0,0 @@ -from nornir_netconf.plugins.tasks import netconf_get_config - - -def test_netconf_get_config(nornir): - """Test get config.""" - nr = nornir.filter(name="netconf_sysrepo") - - result = nr.run(netconf_get_config, source="startup", xmldict=True) - assert result["netconf_sysrepo"].result["ok"] - assert ( - result["netconf_sysrepo"].result["xml_dict"]["data"]["keystore"]["asymmetric-keys"]["asymmetric-key"]["name"] - == "genkey" - ) - - -def test_netconf_get_config_running(nornir): - """Test get running config as default.""" - nr = nornir.filter(name="netconf_sysrepo") - - result = nr.run(netconf_get_config, xmldict=True) - assert result["netconf_sysrepo"].result["ok"] - assert ( - result["netconf_sysrepo"].result["xml_dict"]["data"]["netconf-server"]["listen"]["endpoint"]["name"] - == "default-ssh" - ) - - -def test_netconf_get_config_subtree(nornir): - """Test filter subtree of get_config.""" - nr = nornir.filter(name="netconf_sysrepo") - assert nr.inventory.hosts - - result = nr.run( - netconf_get_config, - source="startup", - path="", - filter_type="subtree", - xmldict=True, - ) - assert result["netconf_sysrepo"].result["ok"] - assert "keystore" in result["netconf_sysrepo"].result["xml_dict"]["data"].keys() - assert "netconf-server" not in result["netconf_sysrepo"].result["xml_dict"]["data"].keys() diff --git a/tests/integration/netopeer2/test_netconf_lock.py b/tests/integration/netopeer2/test_netconf_lock.py deleted file mode 100644 index 588bf2e..0000000 --- a/tests/integration/netopeer2/test_netconf_lock.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Test NETCONF lock - integration.""" -from nornir_netconf.plugins.tasks import netconf_lock - - -def test_netconf_lock(nornir): - """Test Netconf Lock.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_lock, datastore="candidate", operation="lock") - assert result["netconf_sysrepo"].result["ok"] - assert not result["netconf_sysrepo"].result["errors"] - assert not result["netconf_sysrepo"].result["error"] - assert result["netconf_sysrepo"].result["manager"] - assert result["netconf_sysrepo"].result["rpc"] - - -def test_netconf_lock_failed(nornir): - """Test Netconf Lock - failed.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_lock, datastore="candidate", operation="lock") - assert result["netconf_sysrepo"].failed - assert "already locked by this session" in str(result["netconf_sysrepo"].result["error"]) - assert "Unable to find 'ok' or data_xml in response object." in result["netconf_sysrepo"].result["errors"] - assert not result["netconf_sysrepo"].result["ok"] - assert not result["netconf_sysrepo"].result["rpc"] - assert not result["netconf_sysrepo"].result["xml_dict"] diff --git a/tests/integration/nokia_sros/test_sros_integration.py b/tests/integration/nokia_sros/test_sros_integration.py deleted file mode 100644 index af0c0ee..0000000 --- a/tests/integration/nokia_sros/test_sros_integration.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Integration test against SROS device.""" -from nornir_utils.plugins.functions import print_result - -from nornir_netconf.plugins.tasks import ( - netconf_capabilities, - netconf_commit, - netconf_edit_config, - netconf_get, - netconf_get_config, - netconf_lock, -) -from tests.conftest import skip_integration_tests - -DEVICE_NAME = "nokia_rtr" - - -@skip_integration_tests -def test_sros_netconf_capabilities(nornir): - """Test NETCONF Capabilities.""" - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_capabilities) - # print_result(result) - assert "urn:ietf:params:netconf:base:1.0" in result[DEVICE_NAME].result - - -@skip_integration_tests -def test_sros_netconf_get_config(nornir): - """Test get config.""" - nr = nornir.filter(name=DEVICE_NAME) - - result = nr.run( - netconf_get_config, - source="running", - path=""" - - - Base - - - """, - filter_type="subtree", - ) - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["rpc"].data_xml - # with open("tests/test_data/get-sros-config.xml", "w+") as file: - # file.write(result[DEVICE_NAME].result["rpc"].data_xml) - - -@skip_integration_tests -def test_sros_netconf_get(nornir): - """Test NETCONF get operation.""" - nr = nornir.filter(name=DEVICE_NAME) - filter = """ - - - - """ - result = nr.run(netconf_get, filter_type="subtree", path=filter) - assert result[DEVICE_NAME].result - assert result[DEVICE_NAME].result["rpc"].data_xml - - -@skip_integration_tests -def test_sros_netconf_lock_operations(nornir, sros_config_payload): - """Test NETCONF Lock, extract manager and use it to edit-config. - - Afterwards, use netconf_lock with unlock operations to unlock. - """ - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_lock, datastore="candidate", operation="lock") - manager = result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["data_xml"] - # Extract manager from lock operation. - manager = result[DEVICE_NAME].result["manager"] - # print_result(result) - - # Edit Config - result = nr.run(netconf_edit_config, config=sros_config_payload, target="candidate", xmldict=True, manager=manager) - # print_result(result) - assert not result[DEVICE_NAME].result["error"] - assert not result[DEVICE_NAME].result["errors"] - assert "ok/" in result[DEVICE_NAME].result["rpc"].data_xml - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() - - # Commit Config - result = nr.run(netconf_commit, manager=manager, xmldict=True) - # print_result(result) - assert not result[DEVICE_NAME].result["error"] - assert not result[DEVICE_NAME].result["errors"] - assert "ok/" in result[DEVICE_NAME].result["rpc"].data_xml - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() - - # Unlock candidate datastore. - result = nr.run(netconf_lock, datastore="candidate", operation="unlock", manager=manager) - assert result[DEVICE_NAME].result["rpc"] - assert result[DEVICE_NAME].result["manager"] - assert result[DEVICE_NAME].result["data_xml"] - # print_result(result) - - -@skip_integration_tests -def test_sros_netconf_edit_config(nornir, sros_config_payload): - """Test NETCONF edit-config - Post Lock / Unlock operations.""" - nr = nornir.filter(name=DEVICE_NAME) - result = nr.run(netconf_edit_config, config=sros_config_payload, target="candidate", xmldict=True) - assert not result[DEVICE_NAME].result["errors"] - assert "ok/" in result[DEVICE_NAME].result["rpc"].data_xml - assert not result[DEVICE_NAME].result["xml_dict"]["rpc-reply"]["ok"] - print_result(result) - - # Commit Config - result = nr.run(netconf_commit, xmldict=True) - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() diff --git a/tests/integration/nokia_sros/test_sros_netconf_schemas.py b/tests/integration/nokia_sros/test_sros_netconf_schemas.py deleted file mode 100644 index 07aa145..0000000 --- a/tests/integration/nokia_sros/test_sros_netconf_schemas.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Test NETCONF schemas.""" -from nornir_netconf.plugins.tasks import netconf_get_schemas -from tests.conftest import skip_integration_tests - - -@skip_integration_tests -def test_netconf_capabilities_get_schema(nornir, schema_path): - """Test NETCONF Capabilities + Get Schemas success.""" - nr = nornir.filter(name="nokia_rtr") - result = nr.run(netconf_get_schemas, schemas=["nokia-bof-state"], schema_path=schema_path) - assert result["nokia_rtr"].result["log"][0] == "tests/test_data/schema_path/nokia-bof-state.txt created." - assert not result["nokia_rtr"].result["errors"] - - -@skip_integration_tests -def test_netconf_capabilities_get_schema_errors(nornir, schema_path): - """Test NETCONF Capabilities + Get Schemas unrecognized schema name.""" - nr = nornir.filter(name="nokia_rtr") - result = nr.run(netconf_get_schemas, schemas=["nokia-conf-aaa", "ok"], schema_path=schema_path) - assert not result["nokia_rtr"].result["log"] - assert result["nokia_rtr"].result["errors"][0] == "MINOR: MGMT_CORE #2301: Invalid element value" diff --git a/tests/integration/nokia_sros/test_sros_deploy_config.py b/tests/integration/sros/test_sros_edit_config.py similarity index 58% rename from tests/integration/nokia_sros/test_sros_deploy_config.py rename to tests/integration/sros/test_sros_edit_config.py index e04af22..973231b 100644 --- a/tests/integration/nokia_sros/test_sros_deploy_config.py +++ b/tests/integration/sros/test_sros_edit_config.py @@ -1,6 +1,13 @@ -"""Integration Testing Deploying L3VPN via Netconf.""" -from nornir_netconf.plugins.tasks import netconf_commit, netconf_edit_config -from tests.conftest import skip_integration_tests +"""Integration Testing Deploying L3VPN via Netconf to candidate datastore and committing.""" +from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import ( + netconf_commit, + netconf_edit_config, + netconf_get_config, + netconf_validate, +) +from tests.conftest import CONFIGS_DIR, skip_integration_tests, xml_dict DEVICE_NAME = "nokia_rtr" @@ -52,14 +59,20 @@ def test_sros_netconf_edit_config_service(nornir): """Test NETCONF edit-config.""" nr = nornir.filter(name=DEVICE_NAME) - - # Edit Config - result = nr.run(task=netconf_edit_config, target="candidate", config=DEPLOY_SERVICE, xmldict=True) - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() - - # Commit Config - result = nr.run(netconf_commit, xmldict=True) - assert not result[DEVICE_NAME].result["error"] - assert not result[DEVICE_NAME].result["errors"] - assert "ok/" in result[DEVICE_NAME].result["rpc"].data_xml - assert "ok" in result[DEVICE_NAME].result["xml_dict"]["rpc-reply"].keys() + # Edit Candidate Config + result = nr.run(task=netconf_edit_config, target="candidate", config=DEPLOY_SERVICE) + assert not result[DEVICE_NAME].failed + validate = nr.run(task=netconf_validate) + print_result(validate) + # Commit Config into `Running` datastore + result = nr.run(netconf_commit) + assert not result[DEVICE_NAME].failed + # Grab Full Config from datastore + result = nr.run( + netconf_get_config, + source="running", + ) + with open(f"{CONFIGS_DIR}/{DEVICE_NAME}-full-config-post.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "AVIFI-CO" == parsed["rpc-reply"]["data"]["configure"]["service"]["customer"]["customer-name"] diff --git a/tests/integration/sros/test_sros_get.py b/tests/integration/sros/test_sros_get.py new file mode 100644 index 0000000..bbc24c2 --- /dev/null +++ b/tests/integration/sros/test_sros_get.py @@ -0,0 +1,31 @@ +"""Integration test against SROS device.""" +# from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import netconf_get +from tests.conftest import CONFIGS_DIR, skip_integration_tests, xml_dict + +DEVICE_NAME = "nokia_rtr" + + +@skip_integration_tests +def test_sros_netconf_get(nornir): + """Test NETCONF get operation.""" + nr = nornir.filter(name=DEVICE_NAME) + filter = """ + + + 1 + + + """ + + result = nr.run(netconf_get, filter_type="subtree", path=filter) + with open(f"{CONFIGS_DIR}/{DEVICE_NAME}-router-get.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert result[DEVICE_NAME].result + assert ( + "85f24527c381450e926892441835ad7f" + == parsed["rpc-reply"]["data"]["state"]["card"]["hardware-data"]["part-number"] + ) + assert "state" in list(parsed["rpc-reply"]["data"].keys()) diff --git a/tests/integration/sros/test_sros_get_config.py b/tests/integration/sros/test_sros_get_config.py new file mode 100644 index 0000000..24d8f33 --- /dev/null +++ b/tests/integration/sros/test_sros_get_config.py @@ -0,0 +1,46 @@ +"""Integration test against SROS device.""" +# from nornir_utils.plugins.functions import print_result + +from nornir_netconf.plugins.tasks import netconf_get_config +from tests.conftest import CONFIGS_DIR, skip_integration_tests, xml_dict + +DEVICE_NAME = "nokia_rtr" + + +@skip_integration_tests +def test_sros_netconf_get_config(nornir): + """Test get config with subtree.""" + nr = nornir.filter(name=DEVICE_NAME) + + result = nr.run( + netconf_get_config, + source="running", + path=""" + + + + """, + filter_type="subtree", + ) + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + with open(f"{CONFIGS_DIR}/{DEVICE_NAME}-xpath-router-config.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) + parsed = xml_dict(result[DEVICE_NAME].result.rpc.data_xml) + assert "me12-100gb-qsfp28" == parsed["rpc-reply"]["data"]["configure"]["card"]["mda"][0]["mda-type"] + + +@skip_integration_tests +def test_sros_netconf_get_full_config(nornir): + """Test get full config.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run( + netconf_get_config, + source="running", + ) + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME].result.rpc.data_xml + assert not result[DEVICE_NAME].failed + + with open(f"{CONFIGS_DIR}/{DEVICE_NAME}-config.xml", "w+") as file: + file.write(result[DEVICE_NAME].result.rpc.data_xml) diff --git a/tests/inventory_data/groups.yml b/tests/inventory_data/groups.yml index fe1b112..3c229ec 100644 --- a/tests/inventory_data/groups.yml +++ b/tests/inventory_data/groups.yml @@ -1,43 +1,16 @@ --- -netconf_docker: - password: "netconf" - username: "netconf" - platform: "default" +empty_ssh_file_group: + username: "admin" + password: "admin" port: 830 connection_options: netconf: extras: - allow_agent: false hostkey_verify: false - look_for_keys: false - -netconf_docker_ssh: - password: "netconf" - username: "netconf" - port: 830 - connection_options: - netconf: - extras: + timeout: 300 allow_agent: false - hostkey_verify: false look_for_keys: false ssh_config: "tests/inventory_data/ssh_config" - -netconf_docker_nossh: - password: "netconf" - username: "netconf" - platform: "I won't work!" - port: 830 - connection_options: - netconf: - extras: - allow_agent: false - hostkey_verify: false - look_for_keys: false - ssh_config: null - device_params: - name: "default" - sros: username: "admin" password: "admin" @@ -52,7 +25,6 @@ sros: look_for_keys: false device_params: name: "sros" - iosxr: username: "clab" password: "clab@123" @@ -66,8 +38,8 @@ iosxr: allow_agent: false look_for_keys: false csr: - username: "developer" - password: "C1sco12345" + username: "admin" + password: "admin" port: 830 platform: "csr" connection_options: @@ -77,3 +49,15 @@ csr: timeout: 300 allow_agent: false look_for_keys: false +ceos: + username: "admin" + password: "admin" + port: 830 + connection_options: + netconf: + extras: + hostkey_verify: false + timeout: 300 + allow_agent: false + look_for_keys: false +integration: {} diff --git a/tests/inventory_data/hosts.yml b/tests/inventory_data/hosts.yml index c94c5c8..47d685e 100644 --- a/tests/inventory_data/hosts.yml +++ b/tests/inventory_data/hosts.yml @@ -1,35 +1,42 @@ --- -netconf_sysrepo: - hostname: "localhost" - port: 1830 - groups: - - "netconf_docker" -netconf2: - hostname: "netconf2" - port: 1830 - groups: - - "netconf_docker" -netconf3: - hostname: "localhost" - port: 1830 - groups: - - "netconf_docker_ssh" -netconf4: - hostname: "localhost" - port: 1830 - groups: - - "netconf_docker_nossh" nokia_rtr: hostname: "172.200.100.12" port: 830 groups: + - "integration" - "sros" + data: + lock_datastore: "candidate" iosxr_rtr: hostname: "172.200.100.11" port: 830 groups: + - "integration" - "iosxr" + data: + lock_datastore: "candidate" iosxe_rtr: - hostname: "ios-xe-mgmt-latest.cisco.com" + hostname: "172.200.100.13" + groups: + - "integration" + - "csr" + data: + lock_datastore: "running" +ceos: + hostname: "172.200.101.11" + groups: + - "integration" + - "ceos" + data: + vendor: "arista" + lock_datastore: "running" +ceos_empty_ssh_file: + hostname: "172.200.101.11" + groups: + - "empty_ssh_file_group" +devnet_iosxe_rtr: + hostname: "sandbox-iosxe-recomm-1.cisco.com" + username: "developer" + password: "C1sco12345" groups: - "csr" diff --git a/tests/test_data/configs/.gitkeep b/tests/test_data/configs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data/schema_path/nokia-conf-aaa.yang b/tests/test_data/schema_path/nokia-conf-aaa.yang new file mode 100644 index 0000000..8ac27fa --- /dev/null +++ b/tests/test_data/schema_path/nokia-conf-aaa.yang @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/unit/test_helpers_unit.py b/tests/unit/test_helpers_unit.py index 4c262e0..e1a36b7 100644 --- a/tests/unit/test_helpers_unit.py +++ b/tests/unit/test_helpers_unit.py @@ -1,49 +1,34 @@ """Test Helper functions.""" import os import pathlib -from unittest import mock from unittest.mock import patch -import ncclient - from nornir_netconf.plugins.helpers import ( check_capability, + check_file, create_folder, - get_result, write_output, - xml_to_dict, ) -from tests.conftest import FakeRpcObject TEST_FOLDER = "tests/test_data/test_folder_success" - -def test_xml_to_dict_exception(): - """Test xml_to_dict.""" - result = xml_to_dict({"test": "data"}) - - assert result == {"error": "Unable to parse XML to Dict. '.xml' or 'data_xml' not found."} +SRC = str(pathlib.Path(__file__).parent.parent.absolute()) -def test_xml_to_dict_exception_data_xml(): - """Test xml_to_dict. hit data_xml exception on boolean.""" - test_object = FakeRpcObject() - test_object.set_data_xml = True - result = xml_to_dict(test_object) - assert result == {"error": "Unable to parse XML to Dict. a bytes-like object is required, not 'bool'."} +def test_check_file_false(): + """Test check_file false, no file is there..""" + assert not check_file(f"{SRC}/tests/test_data/no_file_here.txt") -def test_xml_to_dict_exception_xml(): - """Test xml_to_dict. hit _xml exception on boolean.""" - test_object = FakeRpcObject() - test_object.set_xml = True - # Delete the data_xml attr to hit exception. - delattr(test_object, "data_xml") - result = xml_to_dict(test_object) - assert result == {"error": "Unable to parse XML to Dict. a bytes-like object is required, not 'bool'."} +def test_check_file_success(): + """Test check_file true.""" + assert not check_file(f"{SRC}/tests/test_data/.gitkeep") -# Test Create Folder +# Ignore type as this function catches typerrors exceptions +def test_check_file_type(): + """Test check_file typeerror.""" + assert not check_file(False) # type: ignore def test_create_folder(test_folder): @@ -85,129 +70,6 @@ def test_write_output_success_already_exists(test_folder): assert os.path.exists(f"{test_folder}/file-name.txt") -# Test Get Results - - -class FakeRpcObjectNoData: - """Test Class.""" - - def __init__(self): - self.ok = False - self.error = "" - self.errors = "" - - -class FakeRpcObjectSlim: - """Test Class.""" - - def __init__(self): - self.error = "" - self.errors = "" - self.data_xml = "" - - -class FakeRpcObjectAny: - """Test Class.""" - - def __init__(self): - self.error = "" - self.errors = "" - - -class FakeRpcObjectXml: - """Test Class.""" - - def __init__(self): - self.error = "" - self.errors = "" - self.data_xml = "" - - -def test_get_result_rpc_slim(): - """Test get result failed, not a Dict, no any 'ok'. - - Get results will re-create the 'ok' attr. - """ - - test_object = FakeRpcObjectSlim() - result = get_result(test_object) - assert not result["failed"] - - -def test_get_result_attr_error(): - """Test get result - attr error.""" - - result = get_result({"name": "prometheus"}) - assert result["failed"] - - -def test_get_result_rpc_ok_no_data_xml(): - """Test get result ok, no data_xml.""" - - test_object = FakeRpcObject() - test_object.set_ok(set=True) - - result = get_result(test_object) - assert not result["failed"] - assert result["result"]["ok"] - - -def test_get_result_ok_false(): - """Test get result ok.""" - - test_object = FakeRpcObject() - test_object.set_ok(set=False) - - result = get_result(test_object) - assert result["failed"] - assert not result["result"]["ok"] - - -@mock.patch.object(ncclient.xml_, "NCElement") -def test_get_result_rpc_ok(nce_element): - """Test get result failed.""" - nce_element.ok = True - - result = get_result(nce_element, xmldict=True) - assert not result["failed"] - assert result["result"]["ok"] - - -def test_get_result_failed(): - """Test get result failed.""" - data = {"ok": False} - result = get_result(data, xmldict=True) - assert result["failed"] - assert not result["result"]["ok"] - assert result["result"]["errors"] == "Unable to find 'ok' or data_xml in response object." - - -@mock.patch.object(ncclient.xml_, "NCElement") -def test_get_result_rpc_no_ok_but_data_xml(nce_element): - """Test get result failed.""" - nce_element.ok = False - nce_element.data_xml = "" - result = get_result(nce_element, xmldict=True) - assert result["failed"] - assert "configure" in result["result"]["xml_dict"].keys() - - -def test_get_result_skip_any(): - """Test get result failed.""" - test_object = FakeRpcObjectAny() - - result = get_result(test_object) - assert result["failed"] - - -def test_get_result_skip_ok_xml_dict(): - """Test get result hit any, skip ok, xmldict.""" - test_object = FakeRpcObjectXml() - - result = get_result(test_object, xmldict=True) - assert not result["failed"] - - capabilities = [ "urn:ietf:params:netconf:base:1.0", "urn:ietf:params:netconf:base:1.1", diff --git a/tests/unit/test_netconf_commit.py b/tests/unit/test_netconf_commit.py index 2546092..d03c85b 100644 --- a/tests/unit/test_netconf_commit.py +++ b/tests/unit/test_netconf_commit.py @@ -1,52 +1,39 @@ """Test NETCONF Commit. -Testing against netconf_sysrepo, fully patched but there is a small bug with patching that conflicts with patching SSH on the next set of tests for edit_config. -Context manager doesn't help, but using a different host does. +Context manager doesn't help, but using a different DEVICE_NAME does. """ from unittest.mock import MagicMock, patch +from nornir_netconf.plugins.helpers import RpcResult from nornir_netconf.plugins.tasks import netconf_commit -from tests.conftest import FakeRpcObject + +DEVICE_NAME = "ceos" @patch("ncclient.manager.connect_ssh") def test_netconf_commit_success(ssh, nornir): """Test success.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'commit' method and response - # as the Fake RPC Object. + response_rpc = MagicMock() response = MagicMock() response.commit.return_value = response_rpc - # Set the SSH session to return the FakeRPC Object when - # performing edit-config call. ssh.return_value = response - # Run Nornir - nr = nornir.filter(name="netconf_sysrepo") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_commit) - assert not result["netconf_sysrepo"].failed - assert not result["netconf_sysrepo"].result["error"] - assert not result["netconf_sysrepo"].result["errors"] - assert result["netconf_sysrepo"].result["rpc"] + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc + assert isinstance(result[DEVICE_NAME].result, RpcResult) @patch("ncclient.manager.connect_ssh") def test_netconf_commit_success_with_manager(ssh, nornir): """Test success with manager.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'commit' method and response - # as the Fake RPC Object. + response_rpc = MagicMock() manager = MagicMock() manager.commit.return_value = response_rpc # Run Nornir - nr = nornir.filter(name="netconf2") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_commit, manager=manager) - assert not result["netconf2"].failed - assert not result["netconf2"].result["error"] - assert not result["netconf2"].result["errors"] - assert result["netconf2"].result["rpc"] + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc ssh.reset_mock() diff --git a/tests/unit/test_netconf_edit_config_unit.py b/tests/unit/test_netconf_edit_config_unit.py index 2cd080f..e38f410 100644 --- a/tests/unit/test_netconf_edit_config_unit.py +++ b/tests/unit/test_netconf_edit_config_unit.py @@ -1,108 +1,80 @@ """Test NETCONF edit-config unit test.""" from unittest.mock import MagicMock, patch -from nornir_utils.plugins.functions import print_result - from nornir_netconf.plugins.tasks import netconf_edit_config -from tests.conftest import FakeRpcObject + +# from nornir_utils.plugins.functions import print_result + + +DEVICE_NAME = "nokia_rtr" @patch("ncclient.manager.connect_ssh") def test_netconf_edit_config_success(ssh, nornir, sros_config_payload): """Test NETCONF edit-config, no defined manager.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'edit-config' method and response - # as the Fake RPC Object. + response_rpc = MagicMock() response = MagicMock() response.server_capabilities = ["netconf:capability:candidate"] response.edit_config.return_value = response_rpc ssh.return_value = response - nr = nornir.filter(name="netconf2") - result = nr.run(netconf_edit_config, target="candidate", config=sros_config_payload) - assert not result["netconf2"].failed - assert result["netconf2"].result["ok"] - assert not result["netconf2"].result["error"] - assert not result["netconf2"].result["errors"] - assert result["netconf2"].result["rpc"] + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_edit_config, target="running", config=sros_config_payload) + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc.ok @patch("ncclient.manager.connect_ssh") def test_netconf_edit_config_manager_set(ssh, nornir, sros_config_payload): """Test NETCONF edit-config, with manager option set.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'edit-config' method and response - # as the Fake RPC Object. + response_rpc = MagicMock() manager = MagicMock() manager.server_capabilities = ["netconf:capability:candidate"] manager.edit_config.return_value = response_rpc - nr = nornir.filter(name="netconf_sysrepo") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_edit_config, target="candidate", config=sros_config_payload, manager=manager) - assert not result["netconf_sysrepo"].failed - assert result["netconf_sysrepo"].result["ok"] - assert not result["netconf_sysrepo"].result["error"] - assert not result["netconf_sysrepo"].result["errors"] - assert result["netconf_sysrepo"].result["rpc"] + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc.ok @patch("ncclient.manager.connect_ssh") def test_netconf_edit_config_bad_operation(ssh, nornir, sros_config_payload): """Test NETCONF edit-config, unsupported default operation.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'edit-config' method and response - # as the Fake RPC Object. + response_rpc = MagicMock(0) response = MagicMock() response.edit_config.return_value = response_rpc ssh.return_value = response - nr = nornir.filter(name="netconf2") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_edit_config, target="candidate", config=sros_config_payload, default_operation="MARGE") - # print_result(result) - assert result["netconf2"].failed + assert result[DEVICE_NAME].failed @patch("ncclient.manager.connect_ssh") def test_netconf_edit_config_success_running(ssh, nornir, sros_config_payload): """Test NETCONF edit-config, no defined manager, no candidate.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() + response_rpc = MagicMock() response_rpc.set_ok(set=True) - # Create a Mock Object. Assign 'edit-config' method and response - # as the Fake RPC Object. response = MagicMock() response.edit_config.return_value = response_rpc ssh.return_value = response - nr = nornir.filter(name="netconf2") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_edit_config, target="running", config=sros_config_payload) - assert not result["netconf2"].failed - assert result["netconf2"].result["ok"] - assert not result["netconf2"].result["error"] - assert not result["netconf2"].result["errors"] - assert result["netconf2"].result["rpc"] + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc.ok @patch("ncclient.manager.connect_ssh") def test_netconf_edit_config_no_capability(ssh, nornir, sros_config_payload): """Test NETCONF edit-config, candidate not supported.""" - # Create Fake RPC Object class. Set 'ok' attr to True. - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=False) - # Create a Mock Object. Assign 'edit-config' method and response - # as the Fake RPC Object. + response_rpc = MagicMock() response = MagicMock() response.server_capabilities = ["netconf:capability:validate:"] response.edit_config.return_value = response_rpc ssh.return_value = response - nr = nornir.filter(name="netconf2") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_edit_config, target="startup", config=sros_config_payload) - assert result["netconf2"].failed - print_result(result) + assert result[DEVICE_NAME].failed diff --git a/tests/unit/test_netconf_lock_unit.py b/tests/unit/test_netconf_lock_unit.py index 22c97e5..933c45e 100644 --- a/tests/unit/test_netconf_lock_unit.py +++ b/tests/unit/test_netconf_lock_unit.py @@ -1,55 +1,46 @@ """Test NETCONF Lock - unit-tests.""" -from unittest.mock import patch +from unittest.mock import MagicMock, patch from nornir_netconf.plugins.tasks import netconf_lock -from tests.conftest import FakeRpcObject -# from nornir_utils.plugins.functions import print_result +DEVICE_NAME = "ceos" def test_netconf_lock(nornir): """Test Netconf Lock, operation not found.""" - nr = nornir.filter(name="netconf_sysrepo") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_lock, datastore="candidate", operation="kock") - assert result["netconf_sysrepo"].failed + assert result[DEVICE_NAME].failed @patch("ncclient.manager.Manager") @patch("ncclient.manager.connect_ssh") def test_netconf_lock_strip_lower(ssh, manager, nornir): """Test Netconf Lock, operation lock success.""" - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) + response_rpc = MagicMock() manager.lock.return_value = response_rpc - - nr = nornir.filter(name="netconf_sysrepo") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_lock, datastore="candidate", operation=" Lock", manager=manager) - assert not result["netconf_sysrepo"].failed - assert result["netconf_sysrepo"].result["manager"] + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc @patch("ncclient.manager.Manager") @patch("ncclient.manager.connect_ssh") def test_netconf_with_manager(ssh, manager, nornir): """Test Netconf Lock, custom manager.""" - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_lock, datastore="candidate", operation=" Lock", manager=manager) - assert result["netconf_sysrepo"].failed - assert result["netconf_sysrepo"].result["manager"] + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_lock, datastore="candidate", operation=" LOCK ", manager=manager) + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc @patch("ncclient.manager.Manager") @patch("ncclient.manager.connect_ssh") def test_netconf_unlock(ssh, manager, nornir): - """Test Netconf Lock, custom manager + data_xml.""" - response_rpc = FakeRpcObject() - response_rpc.set_ok(set=True) - manager.unlock.return_value = response_rpc - - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_lock, datastore="candidate", operation="unlock", manager=manager) - assert not result["netconf_sysrepo"].failed - assert "netconf_unlock" in str(result["netconf_sysrepo"]) - assert result["netconf_sysrepo"].result["manager"] - assert result["netconf_sysrepo"].result["ok"] - assert not result["netconf_sysrepo"].result["data_xml"] # data_xml found in fake_rpc_obj + """Test Netconf UnLock.""" + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_lock, datastore="candidate", operation="unlock") + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc + assert result[DEVICE_NAME][0].name == "netconf_unlock" diff --git a/tests/unit/test_netconf_schemas_unit.py b/tests/unit/test_netconf_schemas_unit.py index 334f7c6..1c61590 100644 --- a/tests/unit/test_netconf_schemas_unit.py +++ b/tests/unit/test_netconf_schemas_unit.py @@ -5,6 +5,9 @@ from nornir_netconf.plugins.tasks import netconf_get_schemas +DEVICE_NAME = "nokia_rtr" + + xml_resp = """ @@ -31,10 +34,10 @@ @patch("ncclient.manager.Manager") def test_netconf_get_schema_schema_path(manager, ssh, nornir): """Test NETCONF Capabilities + Get Schemas success.""" - nr = nornir.filter(name="netconf_sysrepo") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run(netconf_get_schemas, schemas=["nokia-conf-aaa"], schema_path="tests/test_data/schema_path") - assert not result["netconf_sysrepo"].failed - assert result["netconf_sysrepo"].result["log"][0] == "tests/test_data/schema_path/nokia-conf-aaa.txt created." + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.files[0] == "tests/test_data/schema_path/nokia-conf-aaa.yang" @patch("ncclient.manager.connect_ssh") @@ -42,10 +45,9 @@ def test_netconf_get_schema_schema_path(manager, ssh, nornir): def test_netconf_get_schema(manager, ssh, nornir): """Test NETCONF get_schema, missing path""" manager.get_schema.return_value = str("SCHEMA") - nr = nornir.filter(name="netconf_sysrepo") - result = nr.run(netconf_get_schemas, schemas=["nokia-conf-aaa"]) - assert result["netconf_sysrepo"].failed - assert result["netconf_sysrepo"].result["errors"][0] == "Missing 'schema_path' arg to save schema files." + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_get_schemas, schemas=["nokia-conf-aaa"], schema_path="/tmp") + assert result[DEVICE_NAME].result.directory == "/tmp" @patch("ncclient.manager.connect_ssh") @@ -56,8 +58,9 @@ def test_netconf_get_schema_exception(ssh, nornir): # Assign the side_effect to trigger on get_schema call and hit exception. ssh.side_effect = [response] - nr = nornir.filter(name="netconf4") + nr = nornir.filter(name=DEVICE_NAME) result = nr.run( netconf_get_schemas, schemas=["nokia-conf-aaa", "some-other"], schema_path="tests/test_data/schema_path" ) - assert len(result["netconf4"].result["errors"]) == 2 + expected_results = 2 + assert len(result[DEVICE_NAME].result.errors) == expected_results diff --git a/tests/unit/test_netconf_validate.py b/tests/unit/test_netconf_validate.py new file mode 100644 index 0000000..31fbd04 --- /dev/null +++ b/tests/unit/test_netconf_validate.py @@ -0,0 +1,32 @@ +"""Test NETCONF validate unit test.""" +from unittest.mock import MagicMock, patch + +from nornir_netconf.plugins.tasks import netconf_validate + +DEVICE_NAME = "nokia_rtr" + + +@patch("ncclient.manager.connect_ssh") +def test_netconf_netconf_validate_success(ssh, nornir): + """Test NETCONF netconf_validate, no defined manager.""" + response_rpc = MagicMock() + response = MagicMock() + response.validate.return_value = response_rpc + + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_validate, source="running") + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc.ok + + +@patch("ncclient.manager.connect_ssh") +def test_netconf_validate_manager_set(ssh, nornir): + """Test NETCONF edit-config, with manager option set.""" + response_rpc = MagicMock() + manager = MagicMock() + manager.validate.return_value = response_rpc + + nr = nornir.filter(name=DEVICE_NAME) + result = nr.run(netconf_validate, source="candidate", manager=manager) + assert not result[DEVICE_NAME].failed + assert result[DEVICE_NAME].result.rpc.ok diff --git a/tests/unit/test_nornir_hosts_unit.py b/tests/unit/test_nornir_hosts_unit.py index 5c75bf4..8622010 100644 --- a/tests/unit/test_nornir_hosts_unit.py +++ b/tests/unit/test_nornir_hosts_unit.py @@ -1,6 +1,6 @@ -"""Test inventory hosts.""" +"""Test inventory DEVICE_NAMEs.""" -def test_netconf_hosts(nornir): - nr = nornir.filter(name="netconf_sysrepo") - assert "netconf_sysrepo" in nr.inventory.hosts +def test_netconf_device_name(nornir): + nr = nornir.filter(name="ceos") + assert "ceos" in nr.inventory.hosts