Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 3.2 Develop --> Main #151

Merged
merged 16 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/dev/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 3.2.0

* [#144] force the enable call to allow many cisco ios platforms to work
* [#149] Enhanced Jinja Error Handling and Stack Trace Logging by @jmpettit

### New Contributors
* @jmpettit made their first contribution in https://github.com/nautobot/nornir-nautobot/pull/149

**Full Changelog**: https://github.com/nautobot/nornir-nautobot/compare/v3.1.2...v3.2.0

## 3.1.2

- [#145](https://github.com/nautobot/nornir-nautobot/pull/145) Update httpx
Expand Down
9 changes: 8 additions & 1 deletion docs/task/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,11 @@ class DispatcherMixin:
if isinstance(config_context, int):
return config_context
return cls.tcp_port
```
```

## Environment Variables

| Environment Variable | Explanation |
| ----- | ----------- |
| NORNIR_NAUTOBOT_REVERT_IN_SECONDS | Amount in seconds to revert if a config based method fails. |
| NORNIR_NAUTOBOT_NETMIKO_ENABLE_DEFAULT | Override the default(True) to not automatically call the `enable` function before running commands. |
38 changes: 38 additions & 0 deletions examples/basic_with_task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Example with a actual dispatcher task."""

import logging
import os
from nornir import InitNornir
from nornir_utils.plugins.functions import print_result
from nornir_nautobot.plugins.tasks.dispatcher import dispatcher


LOGGER = logging.getLogger(__name__)

my_nornir = InitNornir(
inventory={
"plugin": "NautobotInventory",
"options": {
"nautobot_url": "http://localhost:8080/",
"nautobot_token": "0123456789abcdef0123456789abcdef01234567",
"filter_parameters": {"location": "Site 1"},
"ssl_verify": False,
},
},
)
my_nornir.inventory.defaults.username = os.getenv("NORNIR_USERNAME")
my_nornir.inventory.defaults.password = os.getenv("NORNIR_PASSWORD")

for nr_host, nr_obj in my_nornir.inventory.hosts.items():
network_driver = my_nornir.inventory.hosts[nr_host].platform
result = my_nornir.run(
task=dispatcher,
logger=LOGGER,
method="get_config",
obj=nr_host,
framework="netmiko",
backup_file="./ios.cfg",
remove_lines=None,
substitute_lines=None,
)
print_result(result)
50 changes: 24 additions & 26 deletions nornir_nautobot/plugins/tasks/dispatcher/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
from nornir_jinja2.plugins.tasks import template_file
from nornir_napalm.plugins.tasks import napalm_configure, napalm_get
from nornir_netmiko.tasks import netmiko_send_command

from nornir_nautobot.exceptions import NornirNautobotException
from nornir_nautobot.utils.helpers import make_folder
from nornir_nautobot.utils.helpers import make_folder, get_stack_trace, is_truthy


_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -149,6 +149,7 @@ def generate_config(
jinja_filters: Optional[dict] = None,
jinja_env: Optional[jinja2.Environment] = None,
) -> Result:
# pylint: disable=too-many-locals
"""A small wrapper around template_file Nornir task.

Args:
Expand All @@ -174,29 +175,22 @@ def generate_config(
jinja_env=jinja_env,
)[0].result
except NornirSubTaskError as exc:
if isinstance(exc.result.exception, jinja2.exceptions.UndefinedError): # pylint: disable=no-else-raise
error_msg = (
f"`E1010:` There was a jinja2.exceptions.UndefinedError error: ``{str(exc.result.exception)}``"
)
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

elif isinstance(exc.result.exception, jinja2.TemplateSyntaxError):
error_msg = (f"`E1011:` There was a jinja2.TemplateSyntaxError error: ``{str(exc.result.exception)}``",)
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

elif isinstance(exc.result.exception, jinja2.TemplateNotFound):
error_msg = f"`E1012:` There was an issue finding the template and a jinja2.TemplateNotFound error was raised: ``{str(exc.result.exception)}``"
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

elif isinstance(exc.result.exception, jinja2.TemplateError):
error_msg = f"`E1013:` There was an issue general Jinja error: ``{str(exc.result.exception)}``"
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

error_msg = f"`E1014:` Failed with an unknown issue. `{exc.result.exception}`"
stack_trace = get_stack_trace(exc.result.exception)

error_mapping = {
jinja2.exceptions.UndefinedError: ("E1010", "Undefined variable in Jinja2 template"),
jinja2.TemplateSyntaxError: ("E1011", "Syntax error in Jinja2 template"),
jinja2.TemplateNotFound: ("E1012", "Jinja2 template not found"),
jinja2.TemplateError: ("E1013", "General Jinja2 template error"),
}

for error, (code, message) in error_mapping.items():
if isinstance(exc.result.exception, error):
error_msg = f"`{code}:` {message} - ``{str(exc.result.exception)}``\n```\n{stack_trace}\n```"
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

error_msg = f"`E1014:` Unknown error - `{exc.result.exception}`\n```\n{stack_trace}\n```"
logger.error(error_msg, extra={"object": obj})
raise NornirNautobotException(error_msg)

Expand Down Expand Up @@ -459,7 +453,11 @@ def get_config(
command = cls.config_command

try:
result = task.run(task=netmiko_send_command, command_string=command)
result = task.run(
task=netmiko_send_command,
command_string=command,
enable=is_truthy(os.getenv("NORNIR_NAUTOBOT_NETMIKO_ENABLE_DEFAULT", default="True")),
)
except NornirSubTaskError as exc:
if isinstance(exc.result.exception, NetmikoAuthenticationException):
error_msg = f"`E1017:` Failed with an authentication issue: `{exc.result.exception}`"
Expand Down
29 changes: 29 additions & 0 deletions nornir_nautobot/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import logging
import importlib
import traceback

LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -31,3 +32,31 @@ def import_string(dotted_path):
return getattr(importlib.import_module(module_name), class_name)
except (ModuleNotFoundError, AttributeError):
return None


def get_stack_trace(exc: Exception) -> str:
"""Converts the provided exception's stack trace into a string."""
stack_trace_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
return "".join(stack_trace_lines)


def is_truthy(arg):
"""Convert "truthy" strings into Booleans.

Args:
arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no,
f, false, off and 0. Raises ValueError if val is anything else.

Examples:
>>> is_truthy('yes')
True
"""
if isinstance(arg, bool):
return arg

val = str(arg).lower()
if val in ("y", "yes", "t", "true", "on", "1"):
return True
if val in ("n", "no", "f", "false", "off", "0"):
return False
return True
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "nornir-nautobot"
version = "3.1.1"
version = "3.2.0"
description = "Nornir Nautobot"
authors = ["Network to Code, LLC <[email protected]>"]
readme = "README.md"
Expand Down
Loading