diff --git a/nornir_nautobot/plugins/tasks/dispatcher/default.py b/nornir_nautobot/plugins/tasks/dispatcher/default.py index d1cde23..dad8a2d 100644 --- a/nornir_nautobot/plugins/tasks/dispatcher/default.py +++ b/nornir_nautobot/plugins/tasks/dispatcher/default.py @@ -22,7 +22,7 @@ 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_netmiko.tasks import netmiko_send_command, netmiko_send_config, netmiko_save_config from nornir_nautobot.exceptions import NornirNautobotException from nornir_nautobot.utils.helpers import make_folder, get_stack_trace, is_truthy @@ -497,3 +497,80 @@ def get_config( with open(backup_file, "w", encoding="utf8") as filehandler: filehandler.write(running_config) return Result(host=task.host, result={"config": running_config}) + + @classmethod + def merge_config( + cls, + task: Task, + logger, + obj, + config: str, + ) -> Result: + """Send configuration to merge on the device. + + Args: + task (Task): Nornir Task. + logger (logging.Logger): Logger that may be a Nautobot Jobs or Python logger. + obj (Device): A Nautobot Device Django ORM object instance. + config (str): The config set. + + Raises: + NornirNautobotException: Authentication error. + NornirNautobotException: Timeout error. + NornirNautobotException: Other exception. + + Returns: + Result: Nornir Result object with a dict as a result containing what changed and the result of the push. + """ + logger.info("Config merge via netmiko starting", extra={"object": obj}) + try: + push_result = task.run( + task=netmiko_send_config, + config_commands=config.splitlines(), + enable=True, + ) + except NornirSubTaskError as exc: + if isinstance(exc.result.exception, NetmikoAuthenticationException): + error_msg = f"`E1017:` Failed with an authentication issue: `{exc.result.exception}`" + logger.error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) + + if isinstance(exc.result.exception, NetmikoTimeoutException): + error_msg = f"`E1018:` Failed with a timeout issue. `{exc.result.exception}`" + logger.error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) + + error_msg = f"`E1016:` Failed with an unknown issue. `{exc.result.exception}`" + logger.error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) + + if push_result[0].failed: + return push_result + + # Primarily seen in Cisco devices. + if "Invalid input detected at" in push_result: + error_msg = "`E1019:` Discovered `ERROR: % Invalid input detected at` in the output" + logger.error(error_msg, extra={"object": obj}) + raise NornirNautobotException(error_msg) + + logger.info( + f"result: {push_result[0].result}, changed: {push_result[0].changed}", + extra={"object": obj}, + ) + + if push_result.diff: + logger.info(f"Diff:\n```\n_{push_result[0].diff}\n```", extra={"object": obj}) + + logger.info("Config merge ended", extra={"object": obj}) + try: + task.run( + task=netmiko_save_config, + confirm=True, + ) + except NornirSubTaskError as exc: + error_msg = f"`E1016:`Saving Config Failed with an unknown issue. `{exc.result.exception}`" + logger.error(error_msg, extra={"object": obj}) + return Result( + host=task.host, + result={"changed": push_result[0].changed, "result": push_result[0].result}, + )