diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 90231c7..855d954 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: interval: "weekly" day: "sunday" timezone: "PST8PDT" - time: "07:00" + time: "03:00" target-branch: "develop" - package-ecosystem: "pip" directory: "/" @@ -14,5 +14,5 @@ updates: interval: "weekly" day: "sunday" timezone: "PST8PDT" - time: "07:00" + time: "03:00" target-branch: "develop" diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 54c8705..cb4e1d2 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -27,10 +27,10 @@ jobs: build_posix: runs-on: ${{ matrix.os }} strategy: - max-parallel: 9 + max-parallel: 12 matrix: os: [ubuntu-latest, macos-latest] - version: [3.6, 3.7, 3.8, 3.9] + version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.4] steps: - uses: actions/checkout@v2 - name: set up python ${{ matrix.version }} @@ -42,8 +42,8 @@ jobs: # version we are targeting with nox, while still having versions like 3.9.0a4 run: | echo "FRIENDLY_PYTHON_VERSION=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")" >> $GITHUB_ENV - - name: install libxml2 and libxslt seems to only be needed for 3.9 image for some reason - if: matrix.os == 'ubuntu-latest' && matrix.version == '3.9' + - name: install libxml2 and libxslt seems to only be needed for 3.9+ image for some reason + if: matrix.os == 'ubuntu-latest' && matrix.version == '3.9' || matrix.os == 'ubuntu-latest' && matrix.version == '3.10.0-beta.4' run: | sudo apt install libxml2-dev sudo apt install libxslt-dev @@ -53,4 +53,17 @@ jobs: python -m pip install setuptools python -m pip install nox - name: run nox + env: + # needed to make the terminal a tty (i think? without this system ssh is super broken) + TERM: xterm run: python -m nox -p $FRIENDLY_PYTHON_VERSION -k "not darglint" + + docs-test: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - run: docker run -v $(pwd):/docs --entrypoint "" squidfunk/mkdocs-material:latest ash -c 'pip install mdx_gh_links && mkdocs build --clean --strict' + - name: htmltest + run: | + curl https://htmltest.wjdp.uk | bash + ./bin/htmltest -c docs/htmltest.yml diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index e083476..6718604 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -26,3 +26,9 @@ jobs: run: | python setup.py sdist bdist_wheel python -m twine upload dist/* + - name: create release branch + uses: peterjgrainger/action-create-branch@v2.0.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + branch: ${{ github.event.release.tag_name }} \ No newline at end of file diff --git a/.github/workflows/weekly.yaml b/.github/workflows/weekly.yaml index f612da3..71997f6 100644 --- a/.github/workflows/weekly.yaml +++ b/.github/workflows/weekly.yaml @@ -2,8 +2,8 @@ name: Weekly Build on: schedule: - # weekly at 0700 PST/1400 UTC on Sunday - - cron: '0 14 * * 0' + # weekly at 0300 PST/1000 UTC on Sunday + - cron: '0 10 * * 0' workflow_dispatch: jobs: @@ -31,10 +31,10 @@ jobs: build_posix: runs-on: ${{ matrix.os }} strategy: - max-parallel: 9 + max-parallel: 12 matrix: os: [ubuntu-latest, macos-latest] - version: [3.6, 3.7, 3.8, 3.9] + version: [3.6, 3.7, 3.8, 3.9, 3.10.0-beta.4] steps: - uses: actions/checkout@v2 - name: set up python ${{ matrix.version }} @@ -46,8 +46,8 @@ jobs: # version we are targeting with nox, while still having versions like 3.9.0a4 run: | echo "FRIENDLY_PYTHON_VERSION=$(python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")" >> $GITHUB_ENV - - name: install libxml2 and libxslt seems to only be needed for 3.9 image for some reason - if: matrix.os == 'ubuntu-latest' && matrix.version == '3.9' + - name: install libxml2 and libxslt seems to only be needed for 3.9+ image for some reason + if: matrix.os == 'ubuntu-latest' && matrix.version == '3.9' || matrix.os == 'ubuntu-latest' && matrix.version == '3.10.0-beta4' run: | sudo apt install libxml2-dev sudo apt install libxslt-dev @@ -57,5 +57,7 @@ jobs: python -m pip install setuptools python -m pip install nox - name: run nox - run: python -m nox -p $FRIENDLY_PYTHON_VERSION -k "not darglint" - + env: + # needed to make the terminal a tty (i think? without this system ssh is super broken) + TERM: xterm + run: python -m nox -p $FRIENDLY_PYTHON_VERSION -k "not darglint" \ No newline at end of file diff --git a/.pylintrc b/.pylintrc index 34f9ac9..162d46c 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,7 @@ confidence= # --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=C0103,C0115,C0330,W1202,W1203,R0902,R0913 +disable=C0103,C0115,C0330,R0901,R0902,R0913,W1202,W1203 # C0103 = constant-name (a little too aggressive for some things that aren't "really" constants") # C0115 = class docstrings (init doc strings cover this already) # C0330 = bad-continuation (hanging indent that black doesnt like) @@ -73,6 +73,7 @@ disable=C0103,C0115,C0330,W1202,W1203,R0902,R0913 # W1203 = logging-fstring-interpolation (py3.6, using f-strings so dont care) # R0902 = too-many-instance-attributes # R0913 = too-many-arguments +# R0901 = too-many-ancestors (ignore because it counts mixins and i think thats not useful here) [REPORTS] @@ -398,7 +399,7 @@ int-import-graph= known-standard-library= # Force import order to recognize a module as part of a third party library. -known-third-party=nornir,nornir_utils +known-third-party=enchant # 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 @@ -410,4 +411,4 @@ analyse-fallback-blocks=no # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception \ No newline at end of file +overgeneral-exceptions=Exception diff --git a/LICENSE b/LICENSE index e643fdf..47d60d0 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/Makefile b/Makefile index 8d697ec..59058d1 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,14 @@ lint: python -m black . python -m pylama . python -m pydocstyle . - python -m mypy nornir_scrapli/ + python -m mypy --strict nornir_scrapli/ + +darglint: + find nornir_scrapli -type f \( -iname "*.py"\ ) | xargs darglint -x + +test: + python -m pytest \ + tests/ cov: python -m pytest \ @@ -12,12 +19,25 @@ cov: --cov-report term \ tests/ -test: - python -m pytest tests/ +test_unit: + python -m pytest \ + tests/unit/ + +cov_unit: + python -m pytest \ + --cov=nornir_scrapli \ + --cov-report html \ + --cov-report term \ + tests/unit/ .PHONY: docs docs: python docs/generate/generate_docs.py +test_docs: + mkdocs build --clean --strict + htmltest -c docs/htmltest.yml -s + rm -rf tmp + deploy_docs: mkdocs gh-deploy diff --git a/docs/about/contributing.md b/docs/about/contributing.md index 8cc207a..38095ab 100644 --- a/docs/about/contributing.md +++ b/docs/about/contributing.md @@ -1,16 +1,17 @@ Contributing -======= +============ -Thanks for thinking about contributing to nornir_scrapli! Contributions are not expected, but are quite welcome. +Thanks for thinking about contributing! Contributions are not expected, but are quite welcome. Contributions of all kinds are welcomed -- typos, doc updates, adding examples, bug fixes, and feature adds. Some notes on contributing: -- Please open an issue to discuss any bug fixes, feature adds, or really any thing that could result in a pull - request. This allows us to all be on the same page, and could save everyone some extra work! -- Once we've discussed any changes, pull requests are of course welcome and very much appreciated! - - All PRs should pass tests -- checkout the Makefile for some shortcuts for linting and testing. +- Please open a GitHub discussion topic for any potential feature adds/changes to discuss them prior to opening a PR, + this way everyone has a chance to chime in and make sure we're all on the same page! +- Please open an issue to discuss any bugs/bug fixes prior to opening a PR. +- Once we all have discussed any adds/changes, pull requests are very much welcome and appreciated! + - All PRs should pass tests/CI linting -- checkout the Makefile for some shortcuts for linting and testing. - Please include tests! Even simple/basic tests are better than nothing -- it helps make sure changes in the future don't break functionality or make things act in unexpected ways! diff --git a/docs/api_docs/connection.md b/docs/api_docs/connection.md index 522ee92..71ae4d7 100644 --- a/docs/api_docs/connection.md +++ b/docs/api_docs/connection.md @@ -29,16 +29,22 @@ nornir_scrapli.connection
"""nornir_scrapli.connection""" -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from scrapli import Scrapli from scrapli.driver import GenericDriver from scrapli.exceptions import ScrapliModuleNotFound +from scrapli_cfg import ScrapliCfg +from scrapli_cfg.platform.base.sync_platform import ScrapliCfgPlatform from scrapli_netconf.driver import NetconfDriver from nornir.core.configuration import Config +from nornir.core.task import Task from nornir_scrapli.exceptions import NornirScrapliInvalidPlatform +if TYPE_CHECKING: + from nornir.core.plugins.connections import ConnectionPlugin # pylint: disable=C0412 + CONNECTION_NAME = "scrapli" PLATFORM_MAP = { @@ -77,7 +83,7 @@ class ScrapliCore: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: NornirScrapliInvalidPlatform: if no platform or an invalid scrapli/napalm platform @@ -103,21 +109,20 @@ class ScrapliCore: if not platform: raise NornirScrapliInvalidPlatform( - f"No `platform` provided in inventory for host `{hostname}`" + f"'platform' not provided in inventory for host `{hostname}`" ) - if platform in PLATFORM_MAP: - platform = PLATFORM_MAP.get(platform) + final_platform: str = PLATFORM_MAP.get(platform, platform) - if platform == "generic": + if final_platform == "generic": connection = GenericDriver(**parameters) else: try: - connection = Scrapli(**parameters, platform=platform) # type: ignore + connection = Scrapli(**parameters, platform=final_platform) except ScrapliModuleNotFound as exc: raise NornirScrapliInvalidPlatform( - f"Provided platform `{platform}` is not a valid scrapli or napalm platform, " - "or is not a valid scrapli-community platform." + f"Provided platform `{final_platform}` is not a valid scrapli or napalm " + "platform, or is not a valid scrapli-community platform." ) from exc connection.open() @@ -131,7 +136,7 @@ class ScrapliCore: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -140,6 +145,114 @@ class ScrapliCore: self.connection.close() +class ScrapliConfig: + """Scrapli connection plugin for nornir""" + + connection: ScrapliCfgPlatform + + @staticmethod + def get_connection(task: Task) -> ScrapliCfgPlatform: + """ + Try to fetch scrapli-cfg conn, create it if it doesnt exist + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliCfg + + Raises: + N/A + + """ + connection: ScrapliCfgPlatform + + try: + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + except AttributeError: + task.host.connections["scrapli_cfg"] = ScrapliConfig.spawn(task=task) + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + + return connection + + @staticmethod + def spawn(task: Task) -> "ConnectionPlugin": + """ + Spawn a ScrapliConfig object for a nornir host + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliConfig + + Raises: + N/A + + """ + scrapli_conn = task.host.get_connection("scrapli", task.nornir.config) + scrapli_cfg_parameters = task.host.get_connection_parameters(connection="scrapli_cfg") + + # should always be a dict afaik, but typing doesnt appreciate the possibility it is None + extras = scrapli_cfg_parameters.extras or {} + # always overwrite `dedicated_connection` as we are *not* having a dedicated connection + # since we are wrapping the "normal" scrapli connection! + extras["dedicated_connection"] = False + + final_scrapli_cfg_parameters: Dict[str, Any] = { + "conn": scrapli_conn, + **extras, + } + connection = ScrapliCfg(**final_scrapli_cfg_parameters) + scrapli_config_connection_obj = ScrapliConfig() + scrapli_config_connection_obj.connection = connection + scrapli_config_connection_obj.open() + return scrapli_config_connection_obj + + def open(self, *args: Any, **kwargs: Any) -> None: + """ + Override open method of normal nornir connection so we can coopt an existing conn + + Args: + args: args for not dealing w/ overridden hings + kwargs: kwargs for not dealing w/ overridden hings + + Returns: + None + + Raises: + N/A + + """ + _, _ = args, kwargs + self.connection.prepare() + + def close(self) -> None: + """ + Override close method of normal nornir connection so we never close things + + Never closing allows us to not accidentally step on the underlying "normal" scrapli conn + + Args: + N/A + + Returns: + None + + Raises: + N/A + + """ + + class ScrapliNetconf: """Scrapli NETCONF connection plugin for nornir""" @@ -168,7 +281,7 @@ class ScrapliNetconf: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -205,7 +318,7 @@ class ScrapliNetconf: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -221,6 +334,233 @@ class ScrapliNetconf: ## Classes +### ScrapliConfig + + +```text +Scrapli connection plugin for nornir +``` + +
@@ -208,7 +211,7 @@ Arguments: changed (bool): ``True`` if the task is changing the system diff (obj): Diff between state of the system before/after running this task result (obj): Result of the task execution, see task's documentation for details - host (:obj:`nornir.core.inventory.Host`): Reference to the host that lead ot this result + host (:obj:`nornir.core.inventory.Host`): Reference to the host that lead to this result failed (bool): Whether the execution failed or not severity_level (logging.LEVEL): Severity level associated to the result of the excecution exception (Exception): uncaught exception thrown during the exection of the task (if any) @@ -247,12 +250,12 @@ Raises:++ + +#### Class variables + + +`connection: scrapli_cfg.platform.base.sync_platform.ScrapliCfgPlatform` + + + +#### Static methods + + + +#### get_connection +`get_connection(task: nornir.core.task.Task) ‑> scrapli_cfg.platform.base.sync_platform.ScrapliCfgPlatform` + +```text +Try to fetch scrapli-cfg conn, create it if it doesnt exist + +This is a little different than "normal" in that we dont have a connection and we dont +create them in the "normal" nornir way -- we actually just steal the scrapli connection and +wrap the scrapli_cfg bits around it. + +Args: + task: nornir Task object + +Returns: + ScrapliCfg + +Raises: + N/A +``` + + + + + +#### spawn +`spawn(task: nornir.core.task.Task) ‑> ConnectionPlugin` + +```text +Spawn a ScrapliConfig object for a nornir host + +This is a little different than "normal" in that we dont have a connection and we dont +create them in the "normal" nornir way -- we actually just steal the scrapli connection and +wrap the scrapli_cfg bits around it. + +Args: + task: nornir Task object + +Returns: + ScrapliConfig + +Raises: + N/A +``` + + +#### Methods + + + +##### close +`close(self) ‑> NoneType` + +```text +Override close method of normal nornir connection so we never close things + +Never closing allows us to not accidentally step on the underlying "normal" scrapli conn + +Args: + N/A + +Returns: + None + +Raises: + N/A +``` + + + + + +##### open +`open(self, *args: Any, **kwargs: Any) ‑> NoneType` + +```text +Override open method of normal nornir connection so we can coopt an existing conn + +Args: + args: args for not dealing w/ overridden hings + kwargs: kwargs for not dealing w/ overridden hings + +Returns: + None + +Raises: + N/A +``` + + + + + ### ScrapliCore @@ -261,7 +601,7 @@ class ScrapliCore: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: NornirScrapliInvalidPlatform: if no platform or an invalid scrapli/napalm platform @@ -287,21 +627,20 @@ class ScrapliCore: if not platform: raise NornirScrapliInvalidPlatform( - f"No `platform` provided in inventory for host `{hostname}`" + f"'platform' not provided in inventory for host `{hostname}`" ) - if platform in PLATFORM_MAP: - platform = PLATFORM_MAP.get(platform) + final_platform: str = PLATFORM_MAP.get(platform, platform) - if platform == "generic": + if final_platform == "generic": connection = GenericDriver(**parameters) else: try: - connection = Scrapli(**parameters, platform=platform) # type: ignore + connection = Scrapli(**parameters, platform=final_platform) except ScrapliModuleNotFound as exc: raise NornirScrapliInvalidPlatform( - f"Provided platform `{platform}` is not a valid scrapli or napalm platform, " - "or is not a valid scrapli-community platform." + f"Provided platform `{final_platform}` is not a valid scrapli or napalm " + "platform, or is not a valid scrapli-community platform." ) from exc connection.open() @@ -315,7 +654,7 @@ class ScrapliCore: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -341,7 +680,7 @@ Args: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -352,7 +691,7 @@ Raises: ##### open -`open(self, hostname: Union[str, NoneType], username: Union[str, NoneType], password: Union[str, NoneType], port: Union[int, NoneType], platform: Union[str, NoneType], extras: Union[Dict[str, Any], NoneType] = None, configuration: Union[nornir.core.configuration.Config, NoneType] = None) ‑> NoneType` +`open(self, hostname: Optional[str], username: Optional[str], password: Optional[str], port: Optional[int], platform: Optional[str], extras: Optional[Dict[str, Any]] = None, configuration: Optional[nornir.core.configuration.Config] = None) ‑> NoneType` ```text Open a scrapli connection to a device @@ -368,7 +707,7 @@ Args: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: NornirScrapliInvalidPlatform: if no platform or an invalid scrapli/napalm platform @@ -420,7 +759,7 @@ class ScrapliNetconf: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -457,7 +796,7 @@ class ScrapliNetconf: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -483,7 +822,7 @@ Args: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -494,7 +833,7 @@ Raises: ##### open -`open(self, hostname: Union[str, NoneType], username: Union[str, NoneType], password: Union[str, NoneType], port: Union[int, NoneType], platform: Union[str, NoneType], extras: Union[Dict[str, Any], NoneType] = None, configuration: Union[nornir.core.configuration.Config, NoneType] = None) ‑> NoneType` +`open(self, hostname: Optional[str], username: Optional[str], password: Optional[str], port: Optional[int], platform: Optional[str], extras: Optional[Dict[str, Any]] = None, configuration: Optional[nornir.core.configuration.Config] = None) ‑> NoneType` ```text Open a scrapli connection to a device @@ -511,7 +850,7 @@ Args: configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: N/A diff --git a/docs/api_docs/result.md b/docs/api_docs/result.md index 0714a86..7621f0a 100644 --- a/docs/api_docs/result.md +++ b/docs/api_docs/result.md @@ -32,6 +32,7 @@ nornir_scrapli.result from typing import TYPE_CHECKING, Any, Optional, Union from scrapli.response import MultiResponse, Response +from scrapli_cfg.response import ScrapliCfgResponse from nornir.core.task import Result @@ -88,12 +89,12 @@ def process_config_result(scrapli_response: Union[Response, MultiResponse]) -> s return full_results -class ScrapliResult(Result): # type: ignore +class ScrapliResult(Result): def __init__( self, host: "Host", result: Optional[str], - scrapli_response: Optional[Union[Response, MultiResponse]] = None, + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] = None, changed: bool = False, **kwargs: Any, ): @@ -124,7 +125,9 @@ class ScrapliResult(Result): # type: ignore self.scrapli_response = scrapli_response @staticmethod - def _process_failed(scrapli_response: Optional[Union[Response, MultiResponse]]) -> bool: + def _process_failed( + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] + ) -> bool: """ Process and return string of scrapli response(s) @@ -140,10 +143,10 @@ class ScrapliResult(Result): # type: ignore """ if scrapli_response is None: return False - if isinstance(scrapli_response, Response): + if isinstance(scrapli_response, (Response, ScrapliCfgResponse)): failed: bool = scrapli_response.failed return failed - if any([response.failed for response in scrapli_response]): + if any(response.failed for response in scrapli_response): return True return False+ Expand source code +
++++class ScrapliConfig: + """Scrapli connection plugin for nornir""" + + connection: ScrapliCfgPlatform + + @staticmethod + def get_connection(task: Task) -> ScrapliCfgPlatform: + """ + Try to fetch scrapli-cfg conn, create it if it doesnt exist + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliCfg + + Raises: + N/A + + """ + connection: ScrapliCfgPlatform + + try: + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + except AttributeError: + task.host.connections["scrapli_cfg"] = ScrapliConfig.spawn(task=task) + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + + return connection + + @staticmethod + def spawn(task: Task) -> "ConnectionPlugin": + """ + Spawn a ScrapliConfig object for a nornir host + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliConfig + + Raises: + N/A + + """ + scrapli_conn = task.host.get_connection("scrapli", task.nornir.config) + scrapli_cfg_parameters = task.host.get_connection_parameters(connection="scrapli_cfg") + + # should always be a dict afaik, but typing doesnt appreciate the possibility it is None + extras = scrapli_cfg_parameters.extras or {} + # always overwrite `dedicated_connection` as we are *not* having a dedicated connection + # since we are wrapping the "normal" scrapli connection! + extras["dedicated_connection"] = False + + final_scrapli_cfg_parameters: Dict[str, Any] = { + "conn": scrapli_conn, + **extras, + } + connection = ScrapliCfg(**final_scrapli_cfg_parameters) + scrapli_config_connection_obj = ScrapliConfig() + scrapli_config_connection_obj.connection = connection + scrapli_config_connection_obj.open() + return scrapli_config_connection_obj + + def open(self, *args: Any, **kwargs: Any) -> None: + """ + Override open method of normal nornir connection so we can coopt an existing conn + + Args: + args: args for not dealing w/ overridden hings + kwargs: kwargs for not dealing w/ overridden hings + + Returns: + None + + Raises: + N/A + + """ + _, _ = args, kwargs + self.connection.prepare() + + def close(self) -> None: + """ + Override close method of normal nornir connection so we never close things + + Never closing allows us to not accidentally step on the underlying "normal" scrapli conn + + Args: + N/A + + Returns: + None + + Raises: + N/A + + """ +
+-class ScrapliResult(Result): # type: ignore +class ScrapliResult(Result): def __init__( self, host: "Host", result: Optional[str], - scrapli_response: Optional[Union[Response, MultiResponse]] = None, + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] = None, changed: bool = False, **kwargs: Any, ): @@ -283,7 +286,9 @@ class ScrapliResult(Result): # type: ignore self.scrapli_response = scrapli_response @staticmethod - def _process_failed(scrapli_response: Optional[Union[Response, MultiResponse]]) -> bool: + def _process_failed( + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] + ) -> bool: """ Process and return string of scrapli response(s) @@ -299,10 +304,10 @@ class ScrapliResult(Result): # type: ignore """ if scrapli_response is None: return False - if isinstance(scrapli_response, Response): + if isinstance(scrapli_response, (Response, ScrapliCfgResponse)): failed: bool = scrapli_response.failed return failed - if any([response.failed for response in scrapli_response]): + if any(response.failed for response in scrapli_response): return True return False
diff --git a/docs/api_docs/tasks.md b/docs/api_docs/tasks.md index 1bf2e42..7211e0f 100644 --- a/docs/api_docs/tasks.md +++ b/docs/api_docs/tasks.md @@ -29,27 +29,39 @@ nornir_scrapli.tasks"""nornir_scrapli.tasks""" -from nornir_scrapli.tasks.get_prompt import get_prompt -from nornir_scrapli.tasks.netconf_capabilities import netconf_capabilities -from nornir_scrapli.tasks.netconf_commit import netconf_commit -from nornir_scrapli.tasks.netconf_delete_config import netconf_delete_config -from nornir_scrapli.tasks.netconf_discard import netconf_discard -from nornir_scrapli.tasks.netconf_edit_config import netconf_edit_config -from nornir_scrapli.tasks.netconf_get import netconf_get -from nornir_scrapli.tasks.netconf_get_config import netconf_get_config -from nornir_scrapli.tasks.netconf_lock import netconf_lock -from nornir_scrapli.tasks.netconf_rpc import netconf_rpc -from nornir_scrapli.tasks.netconf_unlock import netconf_unlock -from nornir_scrapli.tasks.netconf_validate import netconf_validate -from nornir_scrapli.tasks.send_command import send_command -from nornir_scrapli.tasks.send_commands import send_commands -from nornir_scrapli.tasks.send_commands_from_file import send_commands_from_file -from nornir_scrapli.tasks.send_config import send_config -from nornir_scrapli.tasks.send_configs import send_configs -from nornir_scrapli.tasks.send_configs_from_file import send_configs_from_file -from nornir_scrapli.tasks.send_interactive import send_interactive +from nornir_scrapli.tasks.cfg.abort_config import cfg_abort_config +from nornir_scrapli.tasks.cfg.commit_config import cfg_commit_config +from nornir_scrapli.tasks.cfg.diff_config import cfg_diff_config +from nornir_scrapli.tasks.cfg.get_config import cfg_get_config +from nornir_scrapli.tasks.cfg.get_version import cfg_get_version +from nornir_scrapli.tasks.cfg.load_config import cfg_load_config +from nornir_scrapli.tasks.core.get_prompt import get_prompt +from nornir_scrapli.tasks.core.send_command import send_command +from nornir_scrapli.tasks.core.send_commands import send_commands +from nornir_scrapli.tasks.core.send_commands_from_file import send_commands_from_file +from nornir_scrapli.tasks.core.send_config import send_config +from nornir_scrapli.tasks.core.send_configs import send_configs +from nornir_scrapli.tasks.core.send_configs_from_file import send_configs_from_file +from nornir_scrapli.tasks.core.send_interactive import send_interactive +from nornir_scrapli.tasks.netconf.capabilities import netconf_capabilities +from nornir_scrapli.tasks.netconf.commit import netconf_commit +from nornir_scrapli.tasks.netconf.delete_config import netconf_delete_config +from nornir_scrapli.tasks.netconf.discard import netconf_discard +from nornir_scrapli.tasks.netconf.edit_config import netconf_edit_config +from nornir_scrapli.tasks.netconf.get import netconf_get +from nornir_scrapli.tasks.netconf.get_config import netconf_get_config +from nornir_scrapli.tasks.netconf.lock import netconf_lock +from nornir_scrapli.tasks.netconf.rpc import netconf_rpc +from nornir_scrapli.tasks.netconf.unlock import netconf_unlock +from nornir_scrapli.tasks.netconf.validate import netconf_validate __all__ = ( + "cfg_abort_config", + "cfg_commit_config", + "cfg_diff_config", + "cfg_get_config", + "cfg_get_version", + "cfg_load_config", "get_prompt", "netconf_capabilities", "netconf_commit", @@ -80,6 +92,149 @@ __all__ = ( +#### cfg_abort_config +`cfg_abort_config(task: nornir.core.task.Task) ‑> nornir.core.task.Result` + +```text +Abort a device candidate config with scrapli-cfg + +Args: + task: nornir task object + +Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + +Raises: + N/A +``` + + + + + + +#### cfg_commit_config +`cfg_commit_config(task: nornir.core.task.Task, source: str = 'running') ‑> nornir.core.task.Result` + +```text +Commit a device candidate config with scrapli-cfg + +Args: + task: nornir task object + source: name of the config source to commit against, generally running|startup + +Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + +Raises: + N/A +``` + + + + + + +#### cfg_diff_config +`cfg_diff_config(task: nornir.core.task.Task, source: str = 'running') ‑> nornir.core.task.Result` + +```text +Diff a device candidate config vs a source config with scrapli-cfg + +The "device diff" is stored as the result. You can access the side by side or unified scrapli +cfg diffs via the "scrapli_response" object stored in the result! + +Args: + task: nornir task object + source: name of the config source to commit against, generally running|startup + +Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + +Raises: + N/A +``` + + + + + + +#### cfg_get_config +`cfg_get_config(task: nornir.core.task.Task, source: str = 'running') ‑> nornir.core.task.Result` + +```text +Get device config with scrapli-cfg + +Args: + task: nornir task object + source: config source to get + +Returns: + Result: nornir result object with Result.result value set to current prompt + +Raises: + N/A +``` + + + + + + +#### cfg_get_version +`cfg_get_version(task: nornir.core.task.Task) ‑> nornir.core.task.Result` + +```text +Get device version with scrapli-cfg + +Args: + task: nornir task object + +Returns: + Result: nornir result object with Result.result value set to current version of device + +Raises: + N/A +``` + + + + + + +#### cfg_load_config +`cfg_load_config(task: nornir.core.task.Task, config: str, replace: bool = False, **kwargs: Any) ‑> nornir.core.task.Result` + +```text +Load device config with scrapli-cfg + +Note that `changed` will still be `False` because this is just loading a candidate config! + +Args: + task: nornir task object + config: string of the configuration to load + replace: replace the configuration or not, if false configuration will be loaded as a + merge operation + kwargs: additional kwargs that the implementing classes may need for their platform, + see your specific platform for details + +Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + +Raises: + N/A +``` + + + + + + #### get_prompt `get_prompt(task: nornir.core.task.Task) ‑> nornir.core.task.Result` @@ -191,7 +346,7 @@ Raises: #### netconf_edit_config -`netconf_edit_config(task: nornir.core.task.Task, config: str, dry_run: Union[bool, NoneType] = None, diff: bool = False, target: str = 'running') ‑> nornir.core.task.Result` +`netconf_edit_config(task: nornir.core.task.Task, config: str, dry_run: Optional[bool] = None, diff: bool = False, target: str = 'running') ‑> nornir.core.task.Result` ```text Edit config from the device with scrapli_netconf @@ -362,7 +517,7 @@ Raises: #### send_command -`send_command(task: nornir.core.task.Task, command: str, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_command(task: nornir.core.task.Task, command: str, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send a single command to device using scrapli @@ -391,7 +546,7 @@ Raises: #### send_commands -`send_commands(task: nornir.core.task.Task, commands: List[str], strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, eager: bool = False, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_commands(task: nornir.core.task.Task, commands: List[str], strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, eager: bool = False, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send a list of commands to device using scrapli @@ -425,7 +580,7 @@ Raises: #### send_commands_from_file -`send_commands_from_file(task: nornir.core.task.Task, file: str, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, eager: bool = False, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_commands_from_file(task: nornir.core.task.Task, file: str, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, eager: bool = False, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send a list of commands from a file to device using scrapli @@ -458,7 +613,7 @@ Raises: #### send_config -`send_config(task: nornir.core.task.Task, config: str, dry_run: Union[bool, NoneType] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_config(task: nornir.core.task.Task, config: str, dry_run: Optional[bool] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send a config to device using scrapli @@ -501,7 +656,7 @@ Raises: #### send_configs -`send_configs(task: nornir.core.task.Task, configs: List[str], dry_run: Union[bool, NoneType] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_configs(task: nornir.core.task.Task, configs: List[str], dry_run: Optional[bool] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send configs to device using scrapli @@ -544,7 +699,7 @@ Raises: #### send_configs_from_file -`send_configs_from_file(task: nornir.core.task.Task, file: str, dry_run: Union[bool, NoneType] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_configs_from_file(task: nornir.core.task.Task, file: str, dry_run: Optional[bool] = None, strip_prompt: bool = True, failed_when_contains: Union[str, List[str], NoneType] = None, stop_on_failed: bool = False, privilege_level: str = '', eager: bool = False, timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send configs from a file to device using scrapli @@ -587,7 +742,7 @@ Raises: #### send_interactive -`send_interactive(task: nornir.core.task.Task, interact_events: List[Tuple[str, str, Union[bool, NoneType]]], failed_when_contains: Union[str, List[str], NoneType] = None, privilege_level: str = '', timeout_ops: Union[float, NoneType] = None) ‑> nornir.core.task.Result` +`send_interactive(task: nornir.core.task.Task, interact_events: List[Tuple[str, str, Optional[bool]]], failed_when_contains: Union[str, List[str], NoneType] = None, privilege_level: str = '', timeout_ops: Optional[float] = None) ‑> nornir.core.task.Result` ```text Send inputs in an interactive fashion using scrapli; usually used to handle prompts diff --git a/docs/generate/mkdocs_markdown.mako b/docs/generate/mkdocs_markdown.mako index 5dccbcc..a2deb1f 100644 --- a/docs/generate/mkdocs_markdown.mako +++ b/docs/generate/mkdocs_markdown.mako @@ -151,12 +151,20 @@ ${module.source} -% if submodules: -<%text>## Sub-modules%text> - % for m in submodules: -* ${m.name} - % endfor -% endif +## % if submodules: +## <%text>## Sub-modules%text> +## % for m in submodules: +## * ${m.name} +## % endfor +## % endif +## +## % if variables: +## <%text>## Variables%text> +## % for v in variables: +## ${variable(v)} +## +## % endfor +## % endif % if functions: <%text>## Functions%text> diff --git a/docs/htmltest.yml b/docs/htmltest.yml new file mode 100644 index 0000000..579c571 --- /dev/null +++ b/docs/htmltest.yml @@ -0,0 +1,12 @@ +# adopted from https://github.com/goreleaser/goreleaser/blob/5adf43295767b5be05fa38a01ffb3ad25bd21797/www/htmltest.yml +# using https://github.com/wjdp/htmltest +DirectoryPath: ./site +IgnoreURLs: + - fonts.gstatic.com + - linkedin.com +IgnoreDirectoryMissingTrailingSlash: true +IgnoreAltMissing: true +IgnoreSSLVerify: true +IgnoreDirs: + - overrides +IgnoreInternalEmptyHash: true diff --git a/docs/more_scrapli/scrapli.md b/docs/more_scrapli/scrapli.md index f680c9b..4de4e8b 100644 --- a/docs/more_scrapli/scrapli.md +++ b/docs/more_scrapli/scrapli.md @@ -1,5 +1,5 @@ -# scrapli +# Scrapli [scrapli](https://github.com/carlmontanari/scrapli) ([docs](https://github.com/carlmontanari/scrapli)) is the -"parent" library on which scrapli_netconf is built. Check it out if you need to connect to devices with telnet or ssh! +"parent" scrapli library. Check it out if you need to connect to devices with telnet or ssh! diff --git a/docs/more_scrapli/scrapli_cfg.md b/docs/more_scrapli/scrapli_cfg.md new file mode 100644 index 0000000..026143e --- /dev/null +++ b/docs/more_scrapli/scrapli_cfg.md @@ -0,0 +1,7 @@ +# Scrapli Cfg + + +[scrapli_cfg](https://github.com/scrapli/scrapli_cfg) ([docs](https://scrapli.github.io/scrapli_cfg/)) +is utility that accepts a scrapli Telnet or SSH connection and provides configuration management capabilities. +scrapli_cfg allows you to load candidate configurations for merge or replace operations, generate diffs of the +current vs candidate, and of course commit or abort the candidate configuration. diff --git a/docs/more_scrapli/scrapli_community.md b/docs/more_scrapli/scrapli_community.md index 6d4eab7..444a48e 100644 --- a/docs/more_scrapli/scrapli_community.md +++ b/docs/more_scrapli/scrapli_community.md @@ -1,4 +1,4 @@ -# scrapli Community +# Scrapli Community If you would like to use scrapli, but the platform(s) that you work with are not supported in the "core" scrapli diff --git a/docs/more_scrapli/scrapli_netconf.md b/docs/more_scrapli/scrapli_netconf.md index 18fa216..025a594 100644 --- a/docs/more_scrapli/scrapli_netconf.md +++ b/docs/more_scrapli/scrapli_netconf.md @@ -1,4 +1,4 @@ -# scrapli Netconf +# Scrapli Netconf [scrapli_netconf](https://github.com/scrapli/scrapli_netconf) ([docs](https://scrapli.github.io/scrapli_netconf/)) diff --git a/docs/more_scrapli/scrapli_replay.md b/docs/more_scrapli/scrapli_replay.md new file mode 100644 index 0000000..7a8aeac --- /dev/null +++ b/docs/more_scrapli/scrapli_replay.md @@ -0,0 +1,7 @@ +# Scrapli Replay + + +[scrapli_replay](https://github.com/scrapli/scrapli_replay) ([docs](https://scrapli.github.io/scrapli_replay/)) +is a set of tools used to help test scrapli programs. scrapli_replay includes a utility to capture command +input/output from real life servers and replay them in a semi-interactive fashion, as well as a pytest plugin that +patches and records and replays session data (like [vcr.py](https://github.com/kevin1024/vcrpy)) for scrapli connections. diff --git a/docs/more_scrapli/scrapli_stubs.md b/docs/more_scrapli/scrapli_stubs.md deleted file mode 100644 index 5f35bce..0000000 --- a/docs/more_scrapli/scrapli_stubs.md +++ /dev/null @@ -1,5 +0,0 @@ -# scrapli Stubs - - -Do you __REALLY__ love typing and scrapli? Great news, type stubs for scrapli are generated periodically and updated -[here](https://github.com/scrapli/scrapli_stubs), enjoy! diff --git a/docs/user_guide/available_functions.md b/docs/user_guide/available_functions.md index 4ad864d..784c4c0 100644 --- a/docs/user_guide/available_functions.md +++ b/docs/user_guide/available_functions.md @@ -1,6 +1,6 @@ # Available Functions -- [print_structured_result](/nornir_scrapli/api_docs/functions/#print_structured_result) -- this function is very similar to the "normal" +- [print_structured_result](https://scrapli.github.io/nornir_scrapli/api_docs/functions/#print_structured_result) -- this function is very similar to the "normal" `print_result` function that now ships with the `nornir_utils` library (historically with nornir "core"), except it contains several additional arguments, most importantly the `parser` argument allows you to select `textfsm` or `genie` to decide which parser to use to parse the unstructured data stored in the results object. Please see the structured diff --git a/docs/user_guide/available_tasks.md b/docs/user_guide/available_tasks.md index 62283ff..73ad10c 100644 --- a/docs/user_guide/available_tasks.md +++ b/docs/user_guide/available_tasks.md @@ -8,28 +8,38 @@ All tasks presented here are methods that live in `scrapli` or `scrapli_netconf` ## Scrapli "core" Tasks -- [get_prompt](/nornir_scrapli/api_docs/tasks/#get_prompt) - Get the current prompt of the device -- [send_command](/nornir_scrapli/api_docs/tasks/#send_command) - Send a single command to the device -- [send_commands](/nornir_scrapli/api_docs/tasks/#send_commands) - Send a list of commands to the device -- [send_commands_from_file](/nornir_scrapli/api_docs/tasks/#send_commands_from_file) - Send a list of commands from a file to the device -- [send_config](/nornir_scrapli/api_docs/tasks/#send_config) - Send a configuration to the device -- [send_configs](/nornir_scrapli/api_docs/tasks/#send_configs) - Send a list of configurations to the device -- [send_configs_from_file](/nornir_scrapli/api_docs/tasks/#send_configs_from_file) - Send a list of configurations from a file to the device -- [send_interactive](/nornir_scrapli/api_docs/tasks/#send_interactive) -"Interact" with the device (handle prompts and inputs and things like that) +- [get_prompt](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#get_prompt) - Get the current prompt of the device +- [send_command](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_command) - Send a single command to the device +- [send_commands](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_commands) - Send a list of commands to the device +- [send_commands_from_file](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_commands_from_file) - Send a list of commands from a file to the device +- [send_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_config) - Send a configuration to the device +- [send_configs](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_configs) - Send a list of configurations to the device +- [send_configs_from_file](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_configs_from_file) - Send a list of configurations from a file to the device +- [send_interactive](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#send_interactive) -"Interact" with the device (handle prompts and inputs and things like that) ## Scrapli Netconf Tasks Note that not all devices will support all operations! -- netconf_capabilities - Get list of capabilities as exchanged during netconf connection establishment -- [netconf_commit](/nornir_scrapli/api_docs/tasks/#commit) - Commit the configuration on the device -- [netconf_discard](/nornir_scrapli/api_docs/tasks/#discard) - Discard the configuration on the device -- [netconf_edit_config](/nornir_scrapli/api_docs/tasks/#edit_config) - Edit the configuration on the device -- [netconf_delete_config](/nornir_scrapli/api_docs/tasks/#delete_config) - Delete a given datastore on the device -- [netconf_get](/nornir_scrapli/api_docs/tasks/#get) - Get a subtree or xpath from the device -- [netconf_get_config](/nornir_scrapli/api_docs/tasks/#get_config) - Get the configuration from the device -- [netconf_lock](/nornir_scrapli/api_docs/tasks/#lock) - Lock the datastore on the device -- [netconf_unlock](/nornir_scrapli/api_docs/tasks/#unlock) - Unlock the datastore on the device -- [netconf_rpc](/nornir_scrapli/api_docs/tasks/#rpc) - Send a "bare" RPC to the device -- [netconf_validate](/nornir_scrapli/api_docs/tasks/#netconf_validate) - Execute the `validate` rpc against a given datastore +- [netconf_capabilities](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#netconf_capabilities/) - Get list of capabilities as exchanged during netconf connection establishment +- [netconf_commit](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#commit) - Commit the configuration on the device +- [netconf_discard](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#discard) - Discard the configuration on the device +- [netconf_edit_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#edit_config) - Edit the configuration on the device +- [netconf_delete_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#delete_config) - Delete a given datastore on the device +- [netconf_get](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#get) - Get a subtree or xpath from the device +- [netconf_get_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#get_config) - Get the configuration from the device +- [netconf_lock](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#lock) - Lock the datastore on the device +- [netconf_unlock](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#unlock) - Unlock the datastore on the device +- [netconf_rpc](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#rpc) - Send a "bare" RPC to the device +- [netconf_validate](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#netconf_validate) - Execute the `validate` rpc against a given datastore + + +## Scrapli Cfg Tasks + +- [cfg_abort_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_abort_config) - Abort a loaded candidate config +- [cfg_commit_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_commit_config) - Commit a loaded candidate config +- [cfg_diff_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_diff_config) - Diff a loaded candidate config +- [cfg_get_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_get_config) - Get a target config +- [cfg_get_version](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_get_version) - Get the device version string +- [cfg_load_config](https://scrapli.github.io/nornir_scrapli/api_docs/tasks/#cfg_load_config) - Load a candidate config diff --git a/docs/user_guide/project_details.md b/docs/user_guide/project_details.md index 80ac3f2..b5b3571 100644 --- a/docs/user_guide/project_details.md +++ b/docs/user_guide/project_details.md @@ -36,7 +36,7 @@ This repo is the nornir plugin for scrapli, however there are other libraries/re - [scrapli](/more_scrapli/scrapli) -- [scrapli_community](/more_scrapli/scrapli_community) - [scrapli_netconf](/more_scrapli/scrapli_netconf) -- [scrapli_stubs](/more_scrapli/scrapli_stubs) - +- [scrapli_community](/more_scrapli/scrapli_community) +- [scrapli_cfg](/more_scrapli/scrapli_cfg) +- [scrapli_replay](/more_scrapli/scrapli_replay) diff --git a/examples/basic_netconf_usage/demo.py b/examples/basic_netconf_usage/demo.py index cd15fc7..85f17e1 100644 --- a/examples/basic_netconf_usage/demo.py +++ b/examples/basic_netconf_usage/demo.py @@ -1,3 +1,4 @@ +"""nornir_scrapli.examples.basic_netconf_usage.demo""" from nornir_utils.plugins.functions.print_result import print_result from nornir import InitNornir @@ -13,6 +14,7 @@ def main() -> None: + """Simple netconf demo!""" nr = InitNornir(config_file="nornir_data/config.yaml") capabilities_result = nr.run(task=netconf_capabilities) @@ -21,7 +23,8 @@ def main() -> None: config_result = nr.run(task=netconf_get_config) print_result(config_result) - filter_ = """
+ filter_ = """ + GigabitEthernet1 @@ -32,7 +35,8 @@ def main() -> None: print_result(result) print("edit-config", "*" * 50) - config = """ + config = """ + GigabitEthernet1 @@ -49,7 +53,8 @@ def main() -> None: result = nr.run(task=netconf_unlock, target="running") print_result(result) - rpc = """+ rpc = """ + GigabitEthernet1 diff --git a/examples/structured_data/demo.py b/examples/structured_data/demo.py index 9e45adb..c1cf4a1 100644 --- a/examples/structured_data/demo.py +++ b/examples/structured_data/demo.py @@ -1,3 +1,4 @@ +"""nornir_scrapli.examples.structured_data.demo""" from nornir_utils.plugins.functions import print_result from nornir import InitNornir @@ -6,6 +7,7 @@ def main() -> None: + """Simple demo for printing structured data""" nr = InitNornir(config_file="nornir_data/config.yaml") show_result = nr.run(task=send_command, command="show version") diff --git a/mkdocs.yml b/mkdocs.yml index 97b3928..b601c48 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,8 @@ --- site_name: Nornir Scrapli -site_url: https://github.com/scrapli/nornir_scrapli -site_description: "Scrapli's plugin for Nornir" +site_description: Scrapli's plugin for Nornir site_author: Carl Montanari +site_url: https://scrapli.github.io/ repo_name: scrapli/nornir_scrapli repo_url: https://github.com/scrapli/nornir_scrapli @@ -17,7 +17,7 @@ theme: repo: fontawesome/brands/github-alt nav: - - Scrapli Nornir: index.md + - Nornir Scrapli: index.md - User Guide: - Quick Start Guide: user_guide/quickstart.md - Project Details: user_guide/project_details.md @@ -37,7 +37,8 @@ nav: - Scrapli: more_scrapli/scrapli.md - Scrapli Netconf: more_scrapli/scrapli_netconf.md - Scrapli Community: more_scrapli/scrapli_community.md - - Scrapli Stubs: more_scrapli/scrapli_stubs.md + - Scrapli Cfg: more_scrapli/scrapli_cfg.md + - Scrapli Replay: more_scrapli/scrapli_replay.md - Other: - Contributing: about/contributing.md - Code of Conduct: about/code_of_conduct.md @@ -51,11 +52,15 @@ markdown_extensions: - mdx_gh_links: user: mkdocs repo: mkdocs + - pymdownx.superfences + - pymdownx.highlight: + use_pygments: True + linenums: True extra: social: - icon: fontawesome/brands/github-alt - link: 'https://github.com/carlmontanari/scrapli' + link: 'https://github.com/carlmontanari/' - icon: fontawesome/brands/twitter link: 'https://twitter.com/carlrmontanari' - icon: fontawesome/brands/linkedin diff --git a/nornir_scrapli/connection.py b/nornir_scrapli/connection.py index da3fae0..446d7ba 100644 --- a/nornir_scrapli/connection.py +++ b/nornir_scrapli/connection.py @@ -1,14 +1,20 @@ """nornir_scrapli.connection""" -from typing import Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional from scrapli import Scrapli from scrapli.driver import GenericDriver from scrapli.exceptions import ScrapliModuleNotFound +from scrapli_cfg import ScrapliCfg +from scrapli_cfg.platform.base.sync_platform import ScrapliCfgPlatform from scrapli_netconf.driver import NetconfDriver from nornir.core.configuration import Config +from nornir.core.task import Task from nornir_scrapli.exceptions import NornirScrapliInvalidPlatform +if TYPE_CHECKING: + from nornir.core.plugins.connections import ConnectionPlugin # pylint: disable=C0412 + CONNECTION_NAME = "scrapli" PLATFORM_MAP = { @@ -47,7 +53,7 @@ def open( configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: NornirScrapliInvalidPlatform: if no platform or an invalid scrapli/napalm platform @@ -73,21 +79,20 @@ def open( if not platform: raise NornirScrapliInvalidPlatform( - f"No `platform` provided in inventory for host `{hostname}`" + f"'platform' not provided in inventory for host `{hostname}`" ) - if platform in PLATFORM_MAP: - platform = PLATFORM_MAP.get(platform) + final_platform: str = PLATFORM_MAP.get(platform, platform) - if platform == "generic": + if final_platform == "generic": connection = GenericDriver(**parameters) else: try: - connection = Scrapli(**parameters, platform=platform) # type: ignore + connection = Scrapli(**parameters, platform=final_platform) except ScrapliModuleNotFound as exc: raise NornirScrapliInvalidPlatform( - f"Provided platform `{platform}` is not a valid scrapli or napalm platform, " - "or is not a valid scrapli-community platform." + f"Provided platform `{final_platform}` is not a valid scrapli or napalm " + "platform, or is not a valid scrapli-community platform." ) from exc connection.open() @@ -101,7 +106,7 @@ def close(self) -> None: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -110,6 +115,114 @@ def close(self) -> None: self.connection.close() +class ScrapliConfig: + """Scrapli connection plugin for nornir""" + + connection: ScrapliCfgPlatform + + @staticmethod + def get_connection(task: Task) -> ScrapliCfgPlatform: + """ + Try to fetch scrapli-cfg conn, create it if it doesnt exist + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliCfg + + Raises: + N/A + + """ + connection: ScrapliCfgPlatform + + try: + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + except AttributeError: + task.host.connections["scrapli_cfg"] = ScrapliConfig.spawn(task=task) + connection = task.host.get_connection("scrapli_cfg", task.nornir.config) + + return connection + + @staticmethod + def spawn(task: Task) -> "ConnectionPlugin": + """ + Spawn a ScrapliConfig object for a nornir host + + This is a little different than "normal" in that we dont have a connection and we dont + create them in the "normal" nornir way -- we actually just steal the scrapli connection and + wrap the scrapli_cfg bits around it. + + Args: + task: nornir Task object + + Returns: + ScrapliConfig + + Raises: + N/A + + """ + scrapli_conn = task.host.get_connection("scrapli", task.nornir.config) + scrapli_cfg_parameters = task.host.get_connection_parameters(connection="scrapli_cfg") + + # should always be a dict afaik, but typing doesnt appreciate the possibility it is None + extras = scrapli_cfg_parameters.extras or {} + # always overwrite `dedicated_connection` as we are *not* having a dedicated connection + # since we are wrapping the "normal" scrapli connection! + extras["dedicated_connection"] = False + + final_scrapli_cfg_parameters: Dict[str, Any] = { + "conn": scrapli_conn, + **extras, + } + connection = ScrapliCfg(**final_scrapli_cfg_parameters) + scrapli_config_connection_obj = ScrapliConfig() + scrapli_config_connection_obj.connection = connection + scrapli_config_connection_obj.open() + return scrapli_config_connection_obj + + def open(self, *args: Any, **kwargs: Any) -> None: + """ + Override open method of normal nornir connection so we can coopt an existing conn + + Args: + args: args for not dealing w/ overridden hings + kwargs: kwargs for not dealing w/ overridden hings + + Returns: + None + + Raises: + N/A + + """ + _, _ = args, kwargs + self.connection.prepare() + + def close(self) -> None: + """ + Override close method of normal nornir connection so we never close things + + Never closing allows us to not accidentally step on the underlying "normal" scrapli conn + + Args: + N/A + + Returns: + None + + Raises: + N/A + + """ + + class ScrapliNetconf: """Scrapli NETCONF connection plugin for nornir""" @@ -138,7 +251,7 @@ def open( configuration: nornir configuration Returns: - N/A # noqa: DAR202 + None Raises: N/A @@ -175,7 +288,7 @@ def close(self) -> None: N/A Returns: - N/A # noqa: DAR202 + None Raises: N/A diff --git a/nornir_scrapli/functions/print_structured_result.py b/nornir_scrapli/functions/print_structured_result.py index 262cbf8..c2d71ff 100644 --- a/nornir_scrapli/functions/print_structured_result.py +++ b/nornir_scrapli/functions/print_structured_result.py @@ -69,7 +69,7 @@ def print_structured_result( if updated_multi_result: updated_agg_result[hostname] = updated_multi_result # noqa - LOCK.acquire() + LOCK.acquire() # pylint: disable=R1732 try: _print_result( result=updated_agg_result, attrs=None, failed=failed, severity_level=severity_level diff --git a/nornir_scrapli/py.typed b/nornir_scrapli/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/nornir_scrapli/result.py b/nornir_scrapli/result.py index d0bb9f0..60848aa 100644 --- a/nornir_scrapli/result.py +++ b/nornir_scrapli/result.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, Any, Optional, Union from scrapli.response import MultiResponse, Response +from scrapli_cfg.response import ScrapliCfgResponse from nornir.core.task import Result @@ -58,12 +59,12 @@ def process_config_result(scrapli_response: Union[Response, MultiResponse]) -> s return full_results -class ScrapliResult(Result): # type: ignore +class ScrapliResult(Result): def __init__( self, host: "Host", result: Optional[str], - scrapli_response: Optional[Union[Response, MultiResponse]] = None, + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] = None, changed: bool = False, **kwargs: Any, ): @@ -94,7 +95,9 @@ def __init__( self.scrapli_response = scrapli_response @staticmethod - def _process_failed(scrapli_response: Optional[Union[Response, MultiResponse]]) -> bool: + def _process_failed( + scrapli_response: Optional[Union[Response, MultiResponse, ScrapliCfgResponse]] + ) -> bool: """ Process and return string of scrapli response(s) @@ -110,7 +113,7 @@ def _process_failed(scrapli_response: Optional[Union[Response, MultiResponse]]) """ if scrapli_response is None: return False - if isinstance(scrapli_response, Response): + if isinstance(scrapli_response, (Response, ScrapliCfgResponse)): failed: bool = scrapli_response.failed return failed if any(response.failed for response in scrapli_response): diff --git a/nornir_scrapli/tasks/__init__.py b/nornir_scrapli/tasks/__init__.py index 45afff3..ad159bc 100644 --- a/nornir_scrapli/tasks/__init__.py +++ b/nornir_scrapli/tasks/__init__.py @@ -1,25 +1,37 @@ """nornir_scrapli.tasks""" -from nornir_scrapli.tasks.get_prompt import get_prompt -from nornir_scrapli.tasks.netconf_capabilities import netconf_capabilities -from nornir_scrapli.tasks.netconf_commit import netconf_commit -from nornir_scrapli.tasks.netconf_delete_config import netconf_delete_config -from nornir_scrapli.tasks.netconf_discard import netconf_discard -from nornir_scrapli.tasks.netconf_edit_config import netconf_edit_config -from nornir_scrapli.tasks.netconf_get import netconf_get -from nornir_scrapli.tasks.netconf_get_config import netconf_get_config -from nornir_scrapli.tasks.netconf_lock import netconf_lock -from nornir_scrapli.tasks.netconf_rpc import netconf_rpc -from nornir_scrapli.tasks.netconf_unlock import netconf_unlock -from nornir_scrapli.tasks.netconf_validate import netconf_validate -from nornir_scrapli.tasks.send_command import send_command -from nornir_scrapli.tasks.send_commands import send_commands -from nornir_scrapli.tasks.send_commands_from_file import send_commands_from_file -from nornir_scrapli.tasks.send_config import send_config -from nornir_scrapli.tasks.send_configs import send_configs -from nornir_scrapli.tasks.send_configs_from_file import send_configs_from_file -from nornir_scrapli.tasks.send_interactive import send_interactive +from nornir_scrapli.tasks.cfg.abort_config import cfg_abort_config +from nornir_scrapli.tasks.cfg.commit_config import cfg_commit_config +from nornir_scrapli.tasks.cfg.diff_config import cfg_diff_config +from nornir_scrapli.tasks.cfg.get_config import cfg_get_config +from nornir_scrapli.tasks.cfg.get_version import cfg_get_version +from nornir_scrapli.tasks.cfg.load_config import cfg_load_config +from nornir_scrapli.tasks.core.get_prompt import get_prompt +from nornir_scrapli.tasks.core.send_command import send_command +from nornir_scrapli.tasks.core.send_commands import send_commands +from nornir_scrapli.tasks.core.send_commands_from_file import send_commands_from_file +from nornir_scrapli.tasks.core.send_config import send_config +from nornir_scrapli.tasks.core.send_configs import send_configs +from nornir_scrapli.tasks.core.send_configs_from_file import send_configs_from_file +from nornir_scrapli.tasks.core.send_interactive import send_interactive +from nornir_scrapli.tasks.netconf.capabilities import netconf_capabilities +from nornir_scrapli.tasks.netconf.commit import netconf_commit +from nornir_scrapli.tasks.netconf.delete_config import netconf_delete_config +from nornir_scrapli.tasks.netconf.discard import netconf_discard +from nornir_scrapli.tasks.netconf.edit_config import netconf_edit_config +from nornir_scrapli.tasks.netconf.get import netconf_get +from nornir_scrapli.tasks.netconf.get_config import netconf_get_config +from nornir_scrapli.tasks.netconf.lock import netconf_lock +from nornir_scrapli.tasks.netconf.rpc import netconf_rpc +from nornir_scrapli.tasks.netconf.unlock import netconf_unlock +from nornir_scrapli.tasks.netconf.validate import netconf_validate __all__ = ( + "cfg_abort_config", + "cfg_commit_config", + "cfg_diff_config", + "cfg_get_config", + "cfg_get_version", + "cfg_load_config", "get_prompt", "netconf_capabilities", "netconf_commit", diff --git a/nornir_scrapli/tasks/cfg/__init__.py b/nornir_scrapli/tasks/cfg/__init__.py new file mode 100644 index 0000000..da0e8fb --- /dev/null +++ b/nornir_scrapli/tasks/cfg/__init__.py @@ -0,0 +1 @@ +"""nornir_scrapli.tasks.cfg""" diff --git a/nornir_scrapli/tasks/cfg/abort_config.py b/nornir_scrapli/tasks/cfg/abort_config.py new file mode 100644 index 0000000..853e8cb --- /dev/null +++ b/nornir_scrapli/tasks/cfg/abort_config.py @@ -0,0 +1,33 @@ +"""nornir_scrapli.tasks.cfg_abort_config""" +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig +from nornir_scrapli.result import ScrapliResult + + +def cfg_abort_config(task: Task) -> Result: + """ + Abort a device candidate config with scrapli-cfg + + Args: + task: nornir task object + + Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + scrapli_response = scrapli_cfg_conn.abort_config() + + result = ScrapliResult( + host=task.host, + result=scrapli_response.result, + scrapli_response=scrapli_response, + changed=False, + ) + + return result diff --git a/nornir_scrapli/tasks/cfg/commit_config.py b/nornir_scrapli/tasks/cfg/commit_config.py new file mode 100644 index 0000000..6948e78 --- /dev/null +++ b/nornir_scrapli/tasks/cfg/commit_config.py @@ -0,0 +1,34 @@ +"""nornir_scrapli.tasks.cfg_commit_config""" +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig +from nornir_scrapli.result import ScrapliResult + + +def cfg_commit_config(task: Task, source: str = "running") -> Result: + """ + Commit a device candidate config with scrapli-cfg + + Args: + task: nornir task object + source: name of the config source to commit against, generally running|startup + + Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + scrapli_response = scrapli_cfg_conn.commit_config(source=source) + + result = ScrapliResult( + host=task.host, + result=scrapli_response.result, + scrapli_response=scrapli_response, + changed=True, + ) + + return result diff --git a/nornir_scrapli/tasks/cfg/diff_config.py b/nornir_scrapli/tasks/cfg/diff_config.py new file mode 100644 index 0000000..274588e --- /dev/null +++ b/nornir_scrapli/tasks/cfg/diff_config.py @@ -0,0 +1,37 @@ +"""nornir_scrapli.tasks.cfg_diff_config""" +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig +from nornir_scrapli.result import ScrapliResult + + +def cfg_diff_config(task: Task, source: str = "running") -> Result: + """ + Diff a device candidate config vs a source config with scrapli-cfg + + The "device diff" is stored as the result. You can access the side by side or unified scrapli + cfg diffs via the "scrapli_response" object stored in the result! + + Args: + task: nornir task object + source: name of the config source to commit against, generally running|startup + + Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + scrapli_response = scrapli_cfg_conn.diff_config(source=source) + + result = ScrapliResult( + host=task.host, + result=scrapli_response.device_diff, + scrapli_response=scrapli_response, + changed=False, + ) + + return result diff --git a/nornir_scrapli/tasks/cfg/get_config.py b/nornir_scrapli/tasks/cfg/get_config.py new file mode 100644 index 0000000..159b580 --- /dev/null +++ b/nornir_scrapli/tasks/cfg/get_config.py @@ -0,0 +1,33 @@ +"""nornir_scrapli.tasks.cfg_get_config""" +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig +from nornir_scrapli.result import ScrapliResult + + +def cfg_get_config(task: Task, source: str = "running") -> Result: + """ + Get device config with scrapli-cfg + + Args: + task: nornir task object + source: config source to get + + Returns: + Result: nornir result object with Result.result value set to current prompt + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + scrapli_response = scrapli_cfg_conn.get_config(source=source) + + result = ScrapliResult( + host=task.host, + result=scrapli_response.result, + scrapli_response=scrapli_response, + changed=False, + ) + + return result diff --git a/nornir_scrapli/tasks/cfg/get_version.py b/nornir_scrapli/tasks/cfg/get_version.py new file mode 100644 index 0000000..ef601d3 --- /dev/null +++ b/nornir_scrapli/tasks/cfg/get_version.py @@ -0,0 +1,24 @@ +"""nornir_scrapli.tasks.cfg.get_version""" +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig + + +def cfg_get_version(task: Task) -> Result: + """ + Get device version with scrapli-cfg + + Args: + task: nornir task object + + Returns: + Result: nornir result object with Result.result value set to current version of device + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + version = scrapli_cfg_conn.get_version() + + return Result(host=task.host, result=version.result, failed=False, changed=False) diff --git a/nornir_scrapli/tasks/cfg/load_config.py b/nornir_scrapli/tasks/cfg/load_config.py new file mode 100644 index 0000000..e133688 --- /dev/null +++ b/nornir_scrapli/tasks/cfg/load_config.py @@ -0,0 +1,42 @@ +"""nornir_scrapli.tasks.cfg_load_config""" +from typing import Any + +from nornir.core.task import Result, Task +from nornir_scrapli.connection import ScrapliConfig +from nornir_scrapli.result import ScrapliResult + + +def cfg_load_config(task: Task, config: str, replace: bool = False, **kwargs: Any) -> Result: + """ + Load device config with scrapli-cfg + + Note that `changed` will still be `False` because this is just loading a candidate config! + + Args: + task: nornir task object + config: string of the configuration to load + replace: replace the configuration or not, if false configuration will be loaded as a + merge operation + kwargs: additional kwargs that the implementing classes may need for their platform, + see your specific platform for details + + Returns: + Result: nornir result object with Result.result value set the string result of the + load_config operation + + Raises: + N/A + + """ + scrapli_cfg_conn = ScrapliConfig.get_connection(task=task) + + scrapli_response = scrapli_cfg_conn.load_config(config=config, replace=replace, **kwargs) + + result = ScrapliResult( + host=task.host, + result=scrapli_response.result, + scrapli_response=scrapli_response, + changed=False, + ) + + return result diff --git a/nornir_scrapli/tasks/core/__init__.py b/nornir_scrapli/tasks/core/__init__.py new file mode 100644 index 0000000..15b0c1c --- /dev/null +++ b/nornir_scrapli/tasks/core/__init__.py @@ -0,0 +1 @@ +"""nornir_scrapli.tasks.core""" diff --git a/nornir_scrapli/tasks/get_prompt.py b/nornir_scrapli/tasks/core/get_prompt.py similarity index 100% rename from nornir_scrapli/tasks/get_prompt.py rename to nornir_scrapli/tasks/core/get_prompt.py diff --git a/nornir_scrapli/tasks/send_command.py b/nornir_scrapli/tasks/core/send_command.py similarity index 100% rename from nornir_scrapli/tasks/send_command.py rename to nornir_scrapli/tasks/core/send_command.py diff --git a/nornir_scrapli/tasks/send_commands.py b/nornir_scrapli/tasks/core/send_commands.py similarity index 100% rename from nornir_scrapli/tasks/send_commands.py rename to nornir_scrapli/tasks/core/send_commands.py diff --git a/nornir_scrapli/tasks/send_commands_from_file.py b/nornir_scrapli/tasks/core/send_commands_from_file.py similarity index 100% rename from nornir_scrapli/tasks/send_commands_from_file.py rename to nornir_scrapli/tasks/core/send_commands_from_file.py diff --git a/nornir_scrapli/tasks/send_config.py b/nornir_scrapli/tasks/core/send_config.py similarity index 100% rename from nornir_scrapli/tasks/send_config.py rename to nornir_scrapli/tasks/core/send_config.py diff --git a/nornir_scrapli/tasks/send_configs.py b/nornir_scrapli/tasks/core/send_configs.py similarity index 100% rename from nornir_scrapli/tasks/send_configs.py rename to nornir_scrapli/tasks/core/send_configs.py diff --git a/nornir_scrapli/tasks/send_configs_from_file.py b/nornir_scrapli/tasks/core/send_configs_from_file.py similarity index 100% rename from nornir_scrapli/tasks/send_configs_from_file.py rename to nornir_scrapli/tasks/core/send_configs_from_file.py diff --git a/nornir_scrapli/tasks/send_interactive.py b/nornir_scrapli/tasks/core/send_interactive.py similarity index 100% rename from nornir_scrapli/tasks/send_interactive.py rename to nornir_scrapli/tasks/core/send_interactive.py diff --git a/nornir_scrapli/tasks/netconf/__init__.py b/nornir_scrapli/tasks/netconf/__init__.py new file mode 100644 index 0000000..b2161ee --- /dev/null +++ b/nornir_scrapli/tasks/netconf/__init__.py @@ -0,0 +1 @@ +"""nornir_scrapli.tasks.netconf""" diff --git a/nornir_scrapli/tasks/netconf_capabilities.py b/nornir_scrapli/tasks/netconf/capabilities.py similarity index 100% rename from nornir_scrapli/tasks/netconf_capabilities.py rename to nornir_scrapli/tasks/netconf/capabilities.py diff --git a/nornir_scrapli/tasks/netconf_commit.py b/nornir_scrapli/tasks/netconf/commit.py similarity index 100% rename from nornir_scrapli/tasks/netconf_commit.py rename to nornir_scrapli/tasks/netconf/commit.py diff --git a/nornir_scrapli/tasks/netconf_delete_config.py b/nornir_scrapli/tasks/netconf/delete_config.py similarity index 100% rename from nornir_scrapli/tasks/netconf_delete_config.py rename to nornir_scrapli/tasks/netconf/delete_config.py diff --git a/nornir_scrapli/tasks/netconf_discard.py b/nornir_scrapli/tasks/netconf/discard.py similarity index 100% rename from nornir_scrapli/tasks/netconf_discard.py rename to nornir_scrapli/tasks/netconf/discard.py diff --git a/nornir_scrapli/tasks/netconf_edit_config.py b/nornir_scrapli/tasks/netconf/edit_config.py similarity index 100% rename from nornir_scrapli/tasks/netconf_edit_config.py rename to nornir_scrapli/tasks/netconf/edit_config.py diff --git a/nornir_scrapli/tasks/netconf_get.py b/nornir_scrapli/tasks/netconf/get.py similarity index 100% rename from nornir_scrapli/tasks/netconf_get.py rename to nornir_scrapli/tasks/netconf/get.py diff --git a/nornir_scrapli/tasks/netconf_get_config.py b/nornir_scrapli/tasks/netconf/get_config.py similarity index 100% rename from nornir_scrapli/tasks/netconf_get_config.py rename to nornir_scrapli/tasks/netconf/get_config.py diff --git a/nornir_scrapli/tasks/netconf_lock.py b/nornir_scrapli/tasks/netconf/lock.py similarity index 100% rename from nornir_scrapli/tasks/netconf_lock.py rename to nornir_scrapli/tasks/netconf/lock.py diff --git a/nornir_scrapli/tasks/netconf_rpc.py b/nornir_scrapli/tasks/netconf/rpc.py similarity index 100% rename from nornir_scrapli/tasks/netconf_rpc.py rename to nornir_scrapli/tasks/netconf/rpc.py diff --git a/nornir_scrapli/tasks/netconf_unlock.py b/nornir_scrapli/tasks/netconf/unlock.py similarity index 100% rename from nornir_scrapli/tasks/netconf_unlock.py rename to nornir_scrapli/tasks/netconf/unlock.py diff --git a/nornir_scrapli/tasks/netconf_validate.py b/nornir_scrapli/tasks/netconf/validate.py similarity index 100% rename from nornir_scrapli/tasks/netconf_validate.py rename to nornir_scrapli/tasks/netconf/validate.py diff --git a/noxfile.py b/noxfile.py index 750831e..6ad4723 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,5 +1,6 @@ """nornir_scrapli.noxfile""" import re +import sys from pathlib import Path from typing import Dict, List @@ -7,35 +8,62 @@ nox.options.error_on_missing_interpreters = False nox.options.stop_on_first_error = False +nox.options.default_venv_backend = "venv" -DEV_REQUIREMENTS: Dict[str, str] = {} -# this wouldn't work for other projects probably as its kinda hacky, but works just fine for scrapli -with open("requirements-dev.txt") as f: - req_lines = f.readlines() - dev_requirements_lines: List[str] = [ +def parse_requirements(dev: bool = True) -> Dict[str, str]: + """ + Parse requirements file + + Args: + dev: parse dev requirements (or not) + + Returns: + dict: dict of parsed requirements + + Raises: + N/A + + """ + requirements = {} + requirements_file = "requirements.txt" if dev is False else "requirements-dev.txt" + + with open(requirements_file, "r") as f: + requirements_file_lines = f.readlines() + + requirements_lines: List[str] = [ line - for line in req_lines + for line in requirements_file_lines if not line.startswith("-r") and not line.startswith("#") and not line.startswith("-e") ] - dev_editable_requirements_lines: List[str] = [ - line for line in req_lines if line.startswith("-e") + editable_requirements_lines: List[str] = [ + line for line in requirements_file_lines if line.startswith("-e") ] -for requirement in dev_requirements_lines: - parsed_requirement = re.match( - pattern=r"^([a-z0-9\-]+)([><=]{1,2}\S*)(?:.*)$", string=requirement, flags=re.I | re.M - ) - DEV_REQUIREMENTS[parsed_requirement.groups()[0]] = parsed_requirement.groups()[1] + for requirement in requirements_lines: + parsed_requirement = re.match( + pattern=r"^([a-z0-9\-\_\.]+)([><=]{1,2}\S*)(?:.*)$", + string=requirement, + flags=re.I | re.M, + ) + requirements[parsed_requirement.groups()[0]] = parsed_requirement.groups()[1] -for requirement in dev_editable_requirements_lines: - parsed_requirement = re.match( - pattern=r"^-e\s.*(?:#egg=)(\w+)$", string=requirement, flags=re.I | re.M - ) - DEV_REQUIREMENTS[parsed_requirement.groups()[0]] = requirement + for requirement in editable_requirements_lines: + parsed_requirement = re.match( + pattern=r"^-e\s.*(?:#egg=)(\w+)$", string=requirement, flags=re.I | re.M + ) + requirements[parsed_requirement.groups()[0]] = requirement + return requirements -@nox.session(python=["3.6", "3.7", "3.8", "3.9"]) + +REQUIREMENTS: Dict[str, str] = parse_requirements(dev=False) +DEV_REQUIREMENTS: Dict[str, str] = parse_requirements(dev=True) +PLATFORM: str = sys.platform +SKIP_LIST: List[str] = [] + + +@nox.session(python=["3.6", "3.7", "3.8", "3.9", "3.10"]) def unit_tests(session): """ Nox run unit tests @@ -44,19 +72,24 @@ def unit_tests(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ - session.install("-e", ".") + if f"unit_tests-{PLATFORM}-{session.python}" in SKIP_LIST: + return + session.install("-r", "requirements-dev.txt") + session.install(".") session.run( + "python", + "-m", "pytest", "--cov=nornir_scrapli", "--cov-report", - "html", + "xml", "--cov-report", "term", "tests/unit", @@ -73,14 +106,14 @@ def isort(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ session.install(f"isort{DEV_REQUIREMENTS['isort']}") - session.run("isort", "-c", ".") + session.run("python", "-m", "isort", "-c", ".") @nox.session(python=["3.9"]) @@ -92,14 +125,14 @@ def black(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ session.install(f"black{DEV_REQUIREMENTS['black']}") - session.run("black", "--check", ".") + session.run("python", "-m", "black", "--check", ".") @nox.session(python=["3.9"]) @@ -111,15 +144,14 @@ def pylama(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ session.install("-r", "requirements-dev.txt") - session.install("-e", ".") - session.run("pylama", ".") + session.run("python", "-m", "pylama", ".") @nox.session(python=["3.9"]) @@ -131,14 +163,14 @@ def pydocstyle(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ session.install(f"pydocstyle{DEV_REQUIREMENTS['pydocstyle']}") - session.run("pydocstyle", ".") + session.run("python", "-m", "pydocstyle", ".") @nox.session(python=["3.9"]) @@ -150,16 +182,15 @@ def mypy(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ + session.install(".") session.install(f"mypy{DEV_REQUIREMENTS['mypy']}") - session.install("-e", DEV_REQUIREMENTS["scrapli_stubs"].split()[1]) - session.env["MYPYPATH"] = f"{session.virtualenv.location}/src/scrapli-stubs" - session.run("mypy", "--strict", "nornir_scrapli/") + session.run("python", "-m", "mypy", "--strict", "nornir_scrapli/") @nox.session(python=["3.9"]) @@ -171,13 +202,12 @@ def darglint(session): session: nox session Returns: - N/A # noqa: DAR202 + None Raises: N/A """ session.install(f"darglint{DEV_REQUIREMENTS['darglint']}") - files_to_darglint = Path("nornir_scrapli").rglob("*.py") - for file in files_to_darglint: + for file in Path("nornir_scrapli").rglob("*.py"): session.run("darglint", f"{file.absolute()}") diff --git a/pyproject.toml b/pyproject.toml index 0970a31..96a9361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,3 @@ [tool.black] line-length = 100 target-version = ['py38'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs - | \.git - | \.hg - | \.mypy_cache - | \.nox - | venv - )/ -) -''' \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt index e000f98..b105bd1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,14 +1,13 @@ -nox==2020.12.31 -black==20.8b1 -isort==5.8.0 -mypy==0.812 -pytest==6.2.2 -pytest-cov==2.11.1 -pylama==7.7.1 +black==21.7b0 +darglint==1.8.0 +isort==5.9.3 +mypy==0.910 +nox==2021.6.12 pycodestyle==2.7.0 -pydocstyle==6.0.0 -pylint==2.7.2 -darglint==1.7.0 --e git+https://github.com/scrapli/scrapli_stubs@master#egg=scrapli_stubs --r requirements.txt +pydocstyle==6.1.1 +pylama==7.7.1 +pylint==2.9.6 +pytest-cov==2.12.1 +pytest==6.2.4 -r requirements-genie.txt +-r requirements.txt \ No newline at end of file diff --git a/requirements-docs.txt b/requirements-docs.txt index 38ab1ae..183e6bb 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,5 +1,5 @@ -pdoc3==0.9.2 ; sys_platform != "win32" -mkdocs==1.1.2 -mkdocs-material==7.0.6 +mdx-gh-links==0.2 +mkdocs==1.2.2 +mkdocs-material==7.2.1 mkdocs-material-extensions==1.0.1 -mdx-gh-links==0.2 \ No newline at end of file +pdoc3==0.9.2 ; sys_platform != "win32" \ No newline at end of file diff --git a/requirements-genie.txt b/requirements-genie.txt index b88aee6..a4fda06 100644 --- a/requirements-genie.txt +++ b/requirements-genie.txt @@ -1,2 +1,2 @@ -genie>=20.2 ; sys_platform != "win32" and python_version < "3.9" -pyats>=20.2 ; sys_platform != "win32" and python_version < "3.9" \ No newline at end of file +genie>=20.2 ; sys_platform != "win32" and python_version < "3.10" +pyats>=20.2 ; sys_platform != "win32" and python_version < "3.10" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7fb1b9c..445cd32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -scrapli>=2021.01.30 -scrapli_netconf>=2021.01.30 +nornir>3.0.0,<4.0.0 +nornir_utils>=0.1.0 +ntc_templates>=1.1.0,<3.0.0 +scrapli>=2021.07.30 +scrapli_cfg>=2021.07.30 scrapli_community>=2021.01.30 -nornir>=3.0.0,<4.0.0 -nornir-utils>=0.1.0 +scrapli_netconf>=2021.01.30 textfsm>=1.1.0,<2.0.0 -ntc_templates>=1.1.0,<3.0.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 5695eac..6c0b20a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,12 @@ +[coverage:run] +source = nornir_scrapli/ + +[coverage:report] +sort = cover + [pylama] linters = mccabe,pycodestyle,pylint -skip = tests/*,.nox/*,venv/*,build/*,private/*,examples/*,docs/*,site/* +skip = .nox/*,build/*,docs/*,private/*,site/*,tests/*,venv/* [pylama:pycodestyle] max_line_length = 100 @@ -9,24 +15,37 @@ max_line_length = 100 rcfile = .pylintrc [pydocstyle] -ignore = D101,D202,D203,D212,D400,D406,D407,D408,D409,D415 match-dir = ^nornir_scrapli/* +ignore = D101,D202,D203,D212,D400,D406,D407,D408,D409,D415 +# D101: missing docstring in public class +# D202: No blank lines allowed after function docstring +# D203: 1 blank line required before class docstring +# D212: Multi-line docstring summary should start at the first line +# D400: First line should end with a period +# D406: Section name should end with a newline +# D407: Missing dashed underline after section +# D408: Section underline should be in the line following the sections name +# D409: Section underline should match the length of its name +# D415: first line should end with a period, question mark, or exclamation point [isort] +profile = black line_length = 100 multi_line_output = 3 include_trailing_comma = True known_first_party = nornir -known_third_party = scrapli,scrapli_netconf,pytest,nornir_utils +known_third_party = nornir_utils,pytest,scrapli,scrapli_netconf [darglint] docstring_style = google strictness = full +ignore = DAR202 [mypy] -python_version = 3.8 +python_version = 3.9 pretty = True ignore_missing_imports = True warn_redundant_casts = True warn_unused_configs = True strict_optional = True + diff --git a/setup.py b/setup.py index 5f8ef06..78da663 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ #!/usr/bin/env python -"""nornir_scrapli - scrapli nornir plugin""" +"""nornir_scrapli""" import setuptools +__version__ = "2021.07.30" __author__ = "Carl Montanari" -__version__ = "2021.01.30" -with open("README.md", "r") as f: +with open("README.md", "r", encoding="utf-8") as f: README = f.read() with open("requirements.txt", "r") as f: @@ -19,32 +19,48 @@ with open(f"requirements-{extra}.txt", "r") as f: EXTRAS_REQUIRE[extra] = f.read().splitlines() +full_requirements = [requirement for extra in EXTRAS_REQUIRE.values() for requirement in extra] +EXTRAS_REQUIRE["full"] = full_requirements + + setuptools.setup( name="nornir_scrapli", version=__version__, author=__author__, author_email="carl.r.montanari@gmail.com", - description="scrapli Nornir plugin", + description="Scrapli's plugin for Nornir", long_description=README, long_description_content_type="text/markdown", + keywords="ssh telnet netconf automation network cisco iosxr iosxe nxos arista eos juniper " + "junos", url="https://github.com/scrapli/nornir_scrapli", + project_urls={ + "Docs": "https://scrapli.github.io/nornir_scrapli/", + }, + license="MIT", + package_data={"nornir_scrapli": ["py.typed"]}, packages=setuptools.find_packages(), install_requires=INSTALL_REQUIRES, + dependency_links=[], extras_require=EXTRAS_REQUIRE, classifiers=[ "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Programming Language :: Python", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development :: Libraries :: Python Modules", ], python_requires=">=3.6", entry_points=""" [nornir.plugins.connections] scrapli=nornir_scrapli.connection:ScrapliCore + scrapli_cfg=nornir_scrapli.connection:ScrapliConfig scrapli_netconf=nornir_scrapli.connection:ScrapliNetconf """, ) diff --git a/tests/unit/functions/test_print_structured_result.py b/tests/unit/functions/test_print_structured_result.py index 9858015..aedfae2 100644 --- a/tests/unit/functions/test_print_structured_result.py +++ b/tests/unit/functions/test_print_structured_result.py @@ -116,11 +116,11 @@ [ ( True, - "\x1b[1m\x1b[36msend_commands*******************************************************************\n\x1b[1m\x1b[34m* sea-ios-1 ** changed : False *************************************************\n\x1b[1m\x1b[32mvvvv send_commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\n[ { 'config_register': '0x2102',\n 'hardware': ['CSR1000V'],\n 'hostname': 'csr1000v',\n 'mac': [],\n 'reload_reason': 'reload',\n 'rommon': 'IOS-XE',\n 'running_image': 'packages.conf',\n 'serial': ['9FKLJWM5EB0'],\n 'uptime': '2 hours, 43 minutes',\n 'version': '16.4.1'}]\n\x1b[1m\x1b[32m---- send_commands ** changed : False ------------------------------------------ INFO\n[ { 'distance': '',\n 'mask': '24',\n 'metric': '',\n 'network': '10.0.0.0',\n 'nexthop_if': 'GigabitEthernet1',\n 'nexthop_ip': '',\n 'protocol': 'C',\n 'type': '',\n 'uptime': ''},\n { 'distance': '',\n 'mask': '32',\n 'metric': '',\n 'network': '10.0.0.15',\n 'nexthop_if': 'GigabitEthernet1',\n 'nexthop_ip': '',\n 'protocol': 'L',\n 'type': '',\n 'uptime': ''}]\n\x1b[1m\x1b[32m^^^^ END send_commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "\x1b[1m\x1b[36msend_commands*******************************************************************\n\x1b[1m\x1b[34m* sea-ios-1 ** changed : False *************************************************\n\x1b[1m\x1b[32mvvvv send_commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\n[ { 'config_register': '0x2102',\n 'hardware': ['CSR1000V'],\n 'hostname': 'csr1000v',\n 'mac': [],\n 'reload_reason': 'reload',\n 'restarted': '',\n 'rommon': 'IOS-XE',\n 'running_image': 'packages.conf',\n 'serial': ['9FKLJWM5EB0'],\n 'uptime': '2 hours, 43 minutes',\n 'version': '16.4.1'}]\n\x1b[1m\x1b[32m---- send_commands ** changed : False ------------------------------------------ INFO\n[ { 'distance': '',\n 'mask': '24',\n 'metric': '',\n 'network': '10.0.0.0',\n 'nexthop_if': 'GigabitEthernet1',\n 'nexthop_ip': '',\n 'protocol': 'C',\n 'type': '',\n 'uptime': ''},\n { 'distance': '',\n 'mask': '32',\n 'metric': '',\n 'network': '10.0.0.15',\n 'nexthop_if': 'GigabitEthernet1',\n 'nexthop_ip': '',\n 'protocol': 'L',\n 'type': '',\n 'uptime': ''}]\n\x1b[1m\x1b[32m^^^^ END send_commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", ), ( False, - "\x1b[1m\x1b[36msend_commands*******************************************************************\n\x1b[1m\x1b[34m* sea-ios-1 ** changed : False *************************************************\n\x1b[1m\x1b[32mvvvv send_commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\n[ [ '16.4.1',\n 'IOS-XE',\n 'csr1000v',\n '2 hours, 43 minutes',\n 'reload',\n 'packages.conf',\n ['CSR1000V'],\n ['9FKLJWM5EB0'],\n '0x2102',\n []]]\n\x1b[1m\x1b[32m---- send_commands ** changed : False ------------------------------------------ INFO\n[ ['C', '', '10.0.0.0', '24', '', '', '', 'GigabitEthernet1', ''],\n ['L', '', '10.0.0.15', '32', '', '', '', 'GigabitEthernet1', '']]\n\x1b[1m\x1b[32m^^^^ END send_commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", + "\x1b[1m\x1b[36msend_commands*******************************************************************\n\x1b[1m\x1b[34m* sea-ios-1 ** changed : False *************************************************\n\x1b[1m\x1b[32mvvvv send_commands ** changed : False vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv INFO\n[ [ '16.4.1',\n 'IOS-XE',\n 'csr1000v',\n '2 hours, 43 minutes',\n 'reload',\n 'packages.conf',\n ['CSR1000V'],\n ['9FKLJWM5EB0'],\n '0x2102',\n [],\n '']]\n\x1b[1m\x1b[32m---- send_commands ** changed : False ------------------------------------------ INFO\n[ ['C', '', '10.0.0.0', '24', '', '', '', 'GigabitEthernet1', ''],\n ['L', '', '10.0.0.15', '32', '', '', '', 'GigabitEthernet1', '']]\n\x1b[1m\x1b[32m^^^^ END send_commands ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n", ), ], ids=["True", "False"], @@ -132,7 +132,7 @@ def test_print_structured_result(capsys, structured_result): @pytest.mark.skipif( - sys.version_info.minor > 8, reason="genie not currently available for python 3.9" + sys.version_info.minor > 9, reason="genie not currently available for python 3.10" ) @pytest.mark.parametrize( "structured_result", @@ -155,7 +155,7 @@ def test_print_structured_result_genie(capsys, structured_result): @pytest.mark.skipif( - sys.version_info.minor > 8, reason="genie not currently available for python 3.9" + sys.version_info.minor > 9, reason="genie not currently available for python 3.10" ) @pytest.mark.parametrize( "structured_result", diff --git a/tests/unit/tasks/cfg/__init__.py b/tests/unit/tasks/cfg/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/tasks/cfg/test_abort_config.py b/tests/unit/tasks/cfg/test_abort_config.py new file mode 100644 index 0000000..af974d9 --- /dev/null +++ b/tests/unit/tasks/cfg/test_abort_config.py @@ -0,0 +1,31 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE +from scrapli_cfg.response import ScrapliCfgResponse + + +def test_abort_config(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_abort_config + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_abort_config(cls): + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"") + cfg_response = ScrapliCfgResponse(host="fake_as_heck") + cfg_response.record_response(scrapli_responses=[response]) + return cfg_response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "abort_config", mock_cfg_abort_config) + + result = nornir.run(task=cfg_abort_config) + # the result is just an empty string because there is not actual "output" from it + assert result["sea-ios-1"].result == "" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is False diff --git a/tests/unit/tasks/cfg/test_commit_config.py b/tests/unit/tasks/cfg/test_commit_config.py new file mode 100644 index 0000000..6789468 --- /dev/null +++ b/tests/unit/tasks/cfg/test_commit_config.py @@ -0,0 +1,32 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE +from scrapli_cfg.response import ScrapliCfgResponse + + +def test_commit_config(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_commit_config + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_commit_config(cls, source): + assert source == "running" + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"") + cfg_response = ScrapliCfgResponse(host="fake_as_heck") + cfg_response.record_response(scrapli_responses=[response]) + return cfg_response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "commit_config", mock_cfg_commit_config) + + result = nornir.run(task=cfg_commit_config) + # the result is just an empty string because there is not actual "output" from it + assert result["sea-ios-1"].result == "" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is True diff --git a/tests/unit/tasks/cfg/test_diff_config.py b/tests/unit/tasks/cfg/test_diff_config.py new file mode 100644 index 0000000..441c02b --- /dev/null +++ b/tests/unit/tasks/cfg/test_diff_config.py @@ -0,0 +1,32 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.diff import ScrapliCfgDiffResponse +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE + + +def test_diff_config(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_diff_config + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_diff_config(cls, source): + assert source == "running" + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"") + cfg_response = ScrapliCfgDiffResponse(host="fake_as_heck", source=source) + cfg_response.record_response(scrapli_responses=[response]) + return cfg_response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "diff_config", mock_cfg_diff_config) + + result = nornir.run(task=cfg_diff_config) + # the result is just an empty string because there is not actual "output" from it + assert result["sea-ios-1"].result == "" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is False diff --git a/tests/unit/tasks/cfg/test_get_config.py b/tests/unit/tasks/cfg/test_get_config.py new file mode 100644 index 0000000..276614e --- /dev/null +++ b/tests/unit/tasks/cfg/test_get_config.py @@ -0,0 +1,31 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE +from scrapli_cfg.response import ScrapliCfgResponse + + +def test_get_config(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_get_config + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_get_config(cls, source): + assert source == "candidate" + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"some stuff about whatever") + cfg_response = ScrapliCfgResponse(host="fake_as_heck") + cfg_response.record_response(scrapli_responses=[response]) + return response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "get_config", mock_cfg_get_config) + + result = nornir.run(task=cfg_get_config, source="candidate") + assert result["sea-ios-1"].result == "some stuff about whatever" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is False diff --git a/tests/unit/tasks/cfg/test_get_version.py b/tests/unit/tasks/cfg/test_get_version.py new file mode 100644 index 0000000..3c6f64e --- /dev/null +++ b/tests/unit/tasks/cfg/test_get_version.py @@ -0,0 +1,30 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE +from scrapli_cfg.response import ScrapliCfgResponse + + +def test_get_version(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_get_version + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_get_version(cls): + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"15.2(4)E7") + cfg_response = ScrapliCfgResponse(host="fake_as_heck") + cfg_response.record_response(scrapli_responses=[response]) + return response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "get_version", mock_cfg_get_version) + + result = nornir.run(task=cfg_get_version) + assert result["sea-ios-1"].result == "15.2(4)E7" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is False diff --git a/tests/unit/tasks/cfg/test_load_config.py b/tests/unit/tasks/cfg/test_load_config.py new file mode 100644 index 0000000..c62811c --- /dev/null +++ b/tests/unit/tasks/cfg/test_load_config.py @@ -0,0 +1,33 @@ +from scrapli.driver.core import IOSXEDriver +from scrapli.response import Response +from scrapli_cfg.platform.core.cisco_iosxe.sync_platform import ScrapliCfgIOSXE +from scrapli_cfg.response import ScrapliCfgResponse + + +def test_load_config(nornir, monkeypatch): + from nornir_scrapli.tasks import cfg_load_config + + def mock_open(cls): + pass + + def mock_cfg_prepare(cls): + pass + + def mock_cfg_load_config(cls, config, replace, **kwargs): + assert config == "configtoload" + assert replace is True + response = Response(host="fake_as_heck", channel_input="blah") + response.record_response(b"") + cfg_response = ScrapliCfgResponse(host="fake_as_heck") + cfg_response.record_response(scrapli_responses=[response]) + return cfg_response + + monkeypatch.setattr(IOSXEDriver, "open", mock_open) + monkeypatch.setattr(ScrapliCfgIOSXE, "prepare", mock_cfg_prepare) + monkeypatch.setattr(ScrapliCfgIOSXE, "load_config", mock_cfg_load_config) + + result = nornir.run(task=cfg_load_config, config="configtoload", replace=True) + # the result is just an empty string because there is not actual "output" from it + assert result["sea-ios-1"].result == "" + assert result["sea-ios-1"].failed is False + assert result["sea-ios-1"].changed is False