From 45271a58f3ca01a16c380e48a890e02a8dd7ce5f Mon Sep 17 00:00:00 2001 From: Benedith Mulongo Date: Tue, 17 Sep 2024 11:32:27 +0200 Subject: [PATCH 01/14] Add get_config_block_regex to extract config section from config --- src/cnaas_nms/tools/jinja_filters.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/cnaas_nms/tools/jinja_filters.py b/src/cnaas_nms/tools/jinja_filters.py index b51cf098..b2379863 100644 --- a/src/cnaas_nms/tools/jinja_filters.py +++ b/src/cnaas_nms/tools/jinja_filters.py @@ -5,6 +5,7 @@ import ipaddress import re from typing import Any, Callable, Optional, Union +from netutils.config.parser import BaseConfigParser, JunosConfigParser # This global dict can be used to update the Jinja environment filters dict to include all # registered template filter function @@ -210,3 +211,30 @@ def sha512(s: str) -> str: def md5(s: str) -> str: """Return SHA256 hexdigest of string s.""" return hashlib.md5(s.encode()).hexdigest() + + +@template_filter() +def get_config_block_regex(config:str, section: str = "firewall", parser: Optional[BaseConfigParser] = JunosConfigParser) -> str: + """ + Get the configuration block for a specific section. + + Args: + config (str): The config used to for parsing and search a specific section. + section (str, optional): The section to retrieve. Defaults to "firewall". Regex can be used as ^(firewall)\s*\{ + parser (str, optional): The parser corresponding to the config type. Defaults to "JunosConfigParser". + + Returns: + str: The text of the configuration block if found, empty string otherwise. + + test: + get_config_block_regex(config=firewall_config, section="firewall") + """ + + config_parser = parser(config) + config_relationship = config_parser.build_config_relationship() + children = config_parser.find_all_children(section, match_type="regex") + + if len(children) > 1: + return "\n".join(children) + "\n}" + + return [] From 89d85da7a4e23c1656a595c62de9c1d2b365e937 Mon Sep 17 00:00:00 2001 From: Benedith Mulongo Date: Tue, 17 Sep 2024 11:49:58 +0200 Subject: [PATCH 02/14] Add conditional in get_config_block_regex --- src/cnaas_nms/tools/jinja_filters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cnaas_nms/tools/jinja_filters.py b/src/cnaas_nms/tools/jinja_filters.py index b2379863..e4e83f8d 100644 --- a/src/cnaas_nms/tools/jinja_filters.py +++ b/src/cnaas_nms/tools/jinja_filters.py @@ -233,8 +233,9 @@ def get_config_block_regex(config:str, section: str = "firewall", parser: Option config_parser = parser(config) config_relationship = config_parser.build_config_relationship() children = config_parser.find_all_children(section, match_type="regex") - + if len(children) > 1: - return "\n".join(children) + "\n}" + collect = "\n".join(children) + return collect + "\n}" if isinstance(config_parser, JunosConfigParser) else collect return [] From 68e8813be207d1c1c58d7c1a1a6c600249c9be55 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 21 Oct 2024 15:37:25 +0200 Subject: [PATCH 03/14] get running configed based on devicetypes in DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG, to be used together with jinja filter like: {{ running_config|get_config_section(firewall, junos) }} --- src/cnaas_nms/app_settings.py | 4 +- src/cnaas_nms/devicehandler/sync_devices.py | 17 ++++++- src/cnaas_nms/tools/jinja_filters.py | 51 +++++++++++++++------ 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/cnaas_nms/app_settings.py b/src/cnaas_nms/app_settings.py index ce64b238..6bd9f1af 100644 --- a/src/cnaas_nms/app_settings.py +++ b/src/cnaas_nms/app_settings.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Optional +from typing import List, Optional import yaml from pydantic import field_validator @@ -57,6 +57,7 @@ class ApiSettings(BaseSettings): COMMIT_CONFIRMED_TIMEOUT: int = 300 COMMIT_CONFIRMED_WAIT: int = 1 SETTINGS_OVERRIDE: Optional[dict] = None + DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG: List[str] = [] @field_validator("MGMTDOMAIN_PRIMARY_IP_VERSION") @classmethod @@ -118,6 +119,7 @@ def construct_api_settings() -> ApiSettings: COMMIT_CONFIRMED_TIMEOUT=config.get("commit_confirmed_timeout", 300), COMMIT_CONFIRMED_WAIT=config.get("commit_confirmed_wait", 1), SETTINGS_OVERRIDE=config.get("settings_override", None), + DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG=config.get("device_types_with_include_running_config", []), ) else: return ApiSettings() diff --git a/src/cnaas_nms/devicehandler/sync_devices.py b/src/cnaas_nms/devicehandler/sync_devices.py index 7a288337..fb4ad704 100644 --- a/src/cnaas_nms/devicehandler/sync_devices.py +++ b/src/cnaas_nms/devicehandler/sync_devices.py @@ -107,7 +107,7 @@ def get_mlag_vars(session, dev: Device) -> dict: def populate_device_vars( - session, dev: Device, ztp_hostname: Optional[str] = None, ztp_devtype: Optional[DeviceType] = None + task, session, dev: Device, ztp_hostname: Optional[str] = None, ztp_devtype: Optional[DeviceType] = None ): logger = get_logger() device_variables = { @@ -360,6 +360,19 @@ def populate_device_vars( ) device_variables = {**device_variables, **fabric_device_variables} + # if device type in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG + for dt_str in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG: + if dev.device_type.name.lower() == dt_str.lower(): + res = task.run(task=napalm_get, getters=["config"]) + + running_config = dict(res.result)["config"]["running"] + # Remove the first task result, which is the napalm_get result, since it's not needed anymore + del task.results[0] + if running_config is None: + raise Exception(f"Failed to get running configuration for {dev.hostname}") + + device_variables["running_config"] = running_config + # Add all environment variables starting with TEMPLATE_SECRET_ to # the list of configuration variables. The idea is to store secret # configuration outside of the templates repository. @@ -513,7 +526,7 @@ def push_sync_device( hostname = task.host.name with sqla_session() as session: dev: Device = session.query(Device).filter(Device.hostname == hostname).one() - template_vars = populate_device_vars(session, dev) + template_vars = populate_device_vars(task, session, dev) platform = dev.platform devtype = dev.device_type diff --git a/src/cnaas_nms/tools/jinja_filters.py b/src/cnaas_nms/tools/jinja_filters.py index e4e83f8d..469d7366 100644 --- a/src/cnaas_nms/tools/jinja_filters.py +++ b/src/cnaas_nms/tools/jinja_filters.py @@ -5,7 +5,15 @@ import ipaddress import re from typing import Any, Callable, Optional, Union -from netutils.config.parser import BaseConfigParser, JunosConfigParser + +from netutils.config.parser import ( + BaseConfigParser, + EOSConfigParser, + IOSConfigParser, + IOSXRConfigParser, + JunosConfigParser, + NXOSConfigParser, +) # This global dict can be used to update the Jinja environment filters dict to include all # registered template filter function @@ -214,28 +222,41 @@ def md5(s: str) -> str: @template_filter() -def get_config_block_regex(config:str, section: str = "firewall", parser: Optional[BaseConfigParser] = JunosConfigParser) -> str: +def get_config_section(config: str, section: str, parser: str) -> str: """ Get the configuration block for a specific section. - + Args: config (str): The config used to for parsing and search a specific section. - section (str, optional): The section to retrieve. Defaults to "firewall". Regex can be used as ^(firewall)\s*\{ - parser (str, optional): The parser corresponding to the config type. Defaults to "JunosConfigParser". - + section (str): The section to retrieve. Regex can be used as "^(firewall)\s*\{" + parser (str): The parser corresponding to the config type, e.g. junos, eos, nxos, iosxr, ios. + Returns: str: The text of the configuration block if found, empty string otherwise. - + test: - get_config_block_regex(config=firewall_config, section="firewall") - """ - - config_parser = parser(config) - config_relationship = config_parser.build_config_relationship() + get_config_section(config=firewall_config, section="firewall", parser="junos") + """ # noqa: W605 + if parser.lower() == "junos": + parser_obj = JunosConfigParser + elif parser.lower() == "eos": + parser_obj = EOSConfigParser + elif parser.lower() == "nxos": + parser_obj = NXOSConfigParser + elif parser.lower() == "iosxr": + parser_obj = IOSXRConfigParser + elif parser.lower() == "ios": + parser_obj = IOSConfigParser + else: + parser_obj = BaseConfigParser + config_parser = parser_obj(config) + config_parser.build_config_relationship() children = config_parser.find_all_children(section, match_type="regex") - + + if len(children) == 1: + return children[0] if len(children) > 1: collect = "\n".join(children) return collect + "\n}" if isinstance(config_parser, JunosConfigParser) else collect - - return [] + + return "" From 53032b2289fd39a2aaee9a8ec5b60be774dd93f4 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 21 Oct 2024 16:26:00 +0200 Subject: [PATCH 04/14] open/close before get config --- src/cnaas_nms/devicehandler/sync_devices.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cnaas_nms/devicehandler/sync_devices.py b/src/cnaas_nms/devicehandler/sync_devices.py index fb4ad704..e3d8e493 100644 --- a/src/cnaas_nms/devicehandler/sync_devices.py +++ b/src/cnaas_nms/devicehandler/sync_devices.py @@ -363,7 +363,9 @@ def populate_device_vars( # if device type in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG for dt_str in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG: if dev.device_type.name.lower() == dt_str.lower(): + task.host.open_connection("napalm", configuration=task.nornir.config) res = task.run(task=napalm_get, getters=["config"]) + task.host.close_connection("napalm") running_config = dict(res.result)["config"]["running"] # Remove the first task result, which is the napalm_get result, since it's not needed anymore From 4ea0b6a291c63805b62bfbf3e8d51bf719974a47 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 10:57:24 +0100 Subject: [PATCH 05/14] when looking for existing worktrees, try to do git pull on each of them and mark devices as unsync if there are updates --- src/cnaas_nms/db/git.py | 4 ++-- src/cnaas_nms/db/git_worktrees.py | 40 ++++++++++++++++++++++++++++--- src/cnaas_nms/db/settings.py | 9 +++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/cnaas_nms/db/git.py b/src/cnaas_nms/db/git.py index ba4b8971..be070ea0 100644 --- a/src/cnaas_nms/db/git.py +++ b/src/cnaas_nms/db/git.py @@ -11,7 +11,7 @@ from cnaas_nms.app_settings import app_settings from cnaas_nms.db.device import Device, DeviceType from cnaas_nms.db.exceptions import ConfigException, RepoStructureException -from cnaas_nms.db.git_worktrees import WorktreeError, clean_templates_worktree +from cnaas_nms.db.git_worktrees import WorktreeError, refresh_existing_templates_worktrees from cnaas_nms.db.job import Job, JobStatus from cnaas_nms.db.joblock import Joblock, JoblockError from cnaas_nms.db.session import redis_session, sqla_session @@ -302,7 +302,7 @@ def _refresh_repo_task(repo_type: RepoType = RepoType.TEMPLATES, job_id: Optiona devtype: DeviceType for devtype, platform in updated_devtypes: Device.set_devtype_syncstatus(session, devtype, ret, "templates", platform, job_id) - clean_templates_worktree() + refresh_existing_templates_worktrees(ret, job_id) return ret diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index 33f3952e..0832b284 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -1,9 +1,13 @@ import os import shutil -from typing import Optional +from typing import List, Optional import git.exc from cnaas_nms.app_settings import app_settings +from cnaas_nms.db.device import Device +from cnaas_nms.db.session import sqla_session +from cnaas_nms.db.settings import get_device_primary_groups, get_groups_using_branch +from cnaas_nms.devicehandler.sync_history import add_sync_event from cnaas_nms.tools.log import get_logger from git import Repo @@ -12,10 +16,40 @@ class WorktreeError(Exception): pass -def clean_templates_worktree(): +def refresh_existing_templates_worktrees(by: str, job_id: int): + """Look for existing worktrees and refresh them""" + logger = get_logger() + updated_groups: List[str] = [] if os.path.isdir("/tmp/worktrees"): for subdir in os.listdir("/tmp/worktrees"): - shutil.rmtree("/tmp/worktrees/" + subdir, ignore_errors=True) + try: + logger.info("Pulling worktree for branch {}".format(subdir)) + wt_repo = Repo("/tmp/worktrees/" + subdir) + diff = wt_repo.remotes.origin.pull() + if not diff: + continue + except Exception as e: + logger.exception(e) + shutil.rmtree("/tmp/worktrees/" + subdir, ignore_errors=True) + updated_groups.append(get_groups_using_branch(subdir)) + + # find all devices that are using these branches and mark them as unsynchronized + updated_hostnames: List[str] = [] + with sqla_session() as session: + for hostname, primary_group in get_device_primary_groups(): + if hostname in updated_hostnames: + continue + if primary_group in updated_groups: + dev: Device = session.query(Device).filter_by(hostname=hostname).one_or_none() + if dev: + dev.synchronized = False + add_sync_event(hostname, "refresh_templates", by, job_id) + updated_hostnames.append(hostname) + logger.debug( + "Devices marked as unsynchronized because git worktree branches were refreshed: {}".format( + ", ".join(updated_hostnames) + ) + ) local_repo = Repo(app_settings.TEMPLATES_LOCAL) local_repo.git.worktree("prune") diff --git a/src/cnaas_nms/db/settings.py b/src/cnaas_nms/db/settings.py index c6354f85..26a249f8 100644 --- a/src/cnaas_nms/db/settings.py +++ b/src/cnaas_nms/db/settings.py @@ -767,6 +767,15 @@ def get_group_templates_branch(group_name: str) -> Optional[str]: return get_group_settings_asdict().get(group_name, {}).get("templates_branch") +def get_groups_using_branch(branch_name: str) -> List[str]: + """Returns a list of group names that use the specified branch name""" + groups = [] + for group_name, group_data in get_group_settings_asdict().items(): + if group_data.get("templates_branch") == branch_name: + groups.append(group_name) + return groups + + @redis_lru_cache def get_group_settings_asdict() -> Dict[str, Dict[str, Any]]: """Returns a dict with group name as key and other parameters as values""" From c578cc4a875c3c8a08a9c58022ecbc10ae44fee4 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 11:19:59 +0100 Subject: [PATCH 06/14] Fix circular imports for now, group things should be moved out of settings module at some point --- src/cnaas_nms/db/git.py | 3 ++- src/cnaas_nms/db/git_worktrees.py | 8 ++++---- src/cnaas_nms/db/groups.py | 14 ++++++++++++++ src/cnaas_nms/db/settings.py | 9 --------- 4 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 src/cnaas_nms/db/groups.py diff --git a/src/cnaas_nms/db/git.py b/src/cnaas_nms/db/git.py index be070ea0..37d41cd4 100644 --- a/src/cnaas_nms/db/git.py +++ b/src/cnaas_nms/db/git.py @@ -20,6 +20,7 @@ SettingsSyntaxError, VlanConflictError, get_device_primary_groups, + get_group_settings_asdict, get_groups, rebuild_settings_cache, ) @@ -302,7 +303,7 @@ def _refresh_repo_task(repo_type: RepoType = RepoType.TEMPLATES, job_id: Optiona devtype: DeviceType for devtype, platform in updated_devtypes: Device.set_devtype_syncstatus(session, devtype, ret, "templates", platform, job_id) - refresh_existing_templates_worktrees(ret, job_id) + refresh_existing_templates_worktrees(ret, job_id, get_group_settings_asdict(), get_device_primary_groups()) return ret diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index 0832b284..1c2dcc14 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -5,8 +5,8 @@ import git.exc from cnaas_nms.app_settings import app_settings from cnaas_nms.db.device import Device +from cnaas_nms.db.groups import get_groups_using_branch from cnaas_nms.db.session import sqla_session -from cnaas_nms.db.settings import get_device_primary_groups, get_groups_using_branch from cnaas_nms.devicehandler.sync_history import add_sync_event from cnaas_nms.tools.log import get_logger from git import Repo @@ -16,7 +16,7 @@ class WorktreeError(Exception): pass -def refresh_existing_templates_worktrees(by: str, job_id: int): +def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: dict, device_primary_groups: dict): """Look for existing worktrees and refresh them""" logger = get_logger() updated_groups: List[str] = [] @@ -31,12 +31,12 @@ def refresh_existing_templates_worktrees(by: str, job_id: int): except Exception as e: logger.exception(e) shutil.rmtree("/tmp/worktrees/" + subdir, ignore_errors=True) - updated_groups.append(get_groups_using_branch(subdir)) + updated_groups.append(get_groups_using_branch(subdir, group_settings)) # find all devices that are using these branches and mark them as unsynchronized updated_hostnames: List[str] = [] with sqla_session() as session: - for hostname, primary_group in get_device_primary_groups(): + for hostname, primary_group in device_primary_groups: if hostname in updated_hostnames: continue if primary_group in updated_groups: diff --git a/src/cnaas_nms/db/groups.py b/src/cnaas_nms/db/groups.py new file mode 100644 index 00000000..47945d00 --- /dev/null +++ b/src/cnaas_nms/db/groups.py @@ -0,0 +1,14 @@ +from typing import List + +# TODO: move all group related things here from settings +# make new settings_helper.py with (verify_dir_structure etc) and separate settings_groups for get_settigns groups? +# use get_group_settings_asdict instead of passing dict in get_groups_using_branch below + + +def get_groups_using_branch(branch_name: str, group_settings: dict) -> List[str]: + """Returns a list of group names that use the specified branch name""" + groups = [] + for group_name, group_data in group_settings.items(): + if group_data.get("templates_branch") == branch_name: + groups.append(group_name) + return groups diff --git a/src/cnaas_nms/db/settings.py b/src/cnaas_nms/db/settings.py index 26a249f8..c6354f85 100644 --- a/src/cnaas_nms/db/settings.py +++ b/src/cnaas_nms/db/settings.py @@ -767,15 +767,6 @@ def get_group_templates_branch(group_name: str) -> Optional[str]: return get_group_settings_asdict().get(group_name, {}).get("templates_branch") -def get_groups_using_branch(branch_name: str) -> List[str]: - """Returns a list of group names that use the specified branch name""" - groups = [] - for group_name, group_data in get_group_settings_asdict().items(): - if group_data.get("templates_branch") == branch_name: - groups.append(group_name) - return groups - - @redis_lru_cache def get_group_settings_asdict() -> Dict[str, Dict[str, Any]]: """Returns a dict with group name as key and other parameters as values""" From 6b7e9f7ec3fc2079028353cc9deb6554ee0f8c45 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 11:22:31 +0100 Subject: [PATCH 07/14] fix syntax --- src/cnaas_nms/db/git_worktrees.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index 1c2dcc14..cc219310 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -36,7 +36,7 @@ def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: d # find all devices that are using these branches and mark them as unsynchronized updated_hostnames: List[str] = [] with sqla_session() as session: - for hostname, primary_group in device_primary_groups: + for hostname, primary_group in device_primary_groups.items(): if hostname in updated_hostnames: continue if primary_group in updated_groups: From f4c4c582ed5dd3dc349d1a42ff9bc3583498ccaf Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 12:48:56 +0100 Subject: [PATCH 08/14] Use set to only have group or hostnames once Only print unsync message if any devices were set to unsync --- src/cnaas_nms/db/git_worktrees.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index cc219310..ba810de3 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -1,6 +1,6 @@ import os import shutil -from typing import List, Optional +from typing import Optional, Set import git.exc from cnaas_nms.app_settings import app_settings @@ -19,7 +19,7 @@ class WorktreeError(Exception): def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: dict, device_primary_groups: dict): """Look for existing worktrees and refresh them""" logger = get_logger() - updated_groups: List[str] = [] + updated_groups: Set[str] = set() if os.path.isdir("/tmp/worktrees"): for subdir in os.listdir("/tmp/worktrees"): try: @@ -31,10 +31,10 @@ def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: d except Exception as e: logger.exception(e) shutil.rmtree("/tmp/worktrees/" + subdir, ignore_errors=True) - updated_groups.append(get_groups_using_branch(subdir, group_settings)) + updated_groups.update(get_groups_using_branch(subdir, group_settings)) # find all devices that are using these branches and mark them as unsynchronized - updated_hostnames: List[str] = [] + updated_hostnames: Set[str] = set() with sqla_session() as session: for hostname, primary_group in device_primary_groups.items(): if hostname in updated_hostnames: @@ -44,12 +44,13 @@ def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: d if dev: dev.synchronized = False add_sync_event(hostname, "refresh_templates", by, job_id) - updated_hostnames.append(hostname) - logger.debug( - "Devices marked as unsynchronized because git worktree branches were refreshed: {}".format( - ", ".join(updated_hostnames) + updated_hostnames.add(hostname) + if updated_hostnames: + logger.debug( + "Devices marked as unsynchronized because git worktree branches were refreshed: {}".format( + ", ".join(updated_hostnames) + ) ) - ) local_repo = Repo(app_settings.TEMPLATES_LOCAL) local_repo.git.worktree("prune") From a810f92bb5b2276ba853080abbdc023b3d2f0a42 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 13:03:10 +0100 Subject: [PATCH 09/14] Get commit message from branch instead of main when updating template branch sync status for devices --- src/cnaas_nms/db/git.py | 2 +- src/cnaas_nms/db/git_worktrees.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/cnaas_nms/db/git.py b/src/cnaas_nms/db/git.py index 37d41cd4..775fb4f5 100644 --- a/src/cnaas_nms/db/git.py +++ b/src/cnaas_nms/db/git.py @@ -303,7 +303,7 @@ def _refresh_repo_task(repo_type: RepoType = RepoType.TEMPLATES, job_id: Optiona devtype: DeviceType for devtype, platform in updated_devtypes: Device.set_devtype_syncstatus(session, devtype, ret, "templates", platform, job_id) - refresh_existing_templates_worktrees(ret, job_id, get_group_settings_asdict(), get_device_primary_groups()) + refresh_existing_templates_worktrees(job_id, get_group_settings_asdict(), get_device_primary_groups()) return ret diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index ba810de3..322ae2e3 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -16,7 +16,7 @@ class WorktreeError(Exception): pass -def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: dict, device_primary_groups: dict): +def refresh_existing_templates_worktrees(job_id: int, group_settings: dict, device_primary_groups: dict): """Look for existing worktrees and refresh them""" logger = get_logger() updated_groups: Set[str] = set() @@ -28,6 +28,19 @@ def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: d diff = wt_repo.remotes.origin.pull() if not diff: continue + + ret: str = "" + for item in diff: + # only check for changes in our branch + if item.ref.remote_head != wt_repo.head.ref.name: + continue + + ret += "Commit {} by {} at {}\n".format( + item.commit.name_rev, item.commit.committer, item.commit.committed_datetime + ) + # don't update updated_groups if changes were only in other branches + if not ret: + continue except Exception as e: logger.exception(e) shutil.rmtree("/tmp/worktrees/" + subdir, ignore_errors=True) @@ -43,7 +56,7 @@ def refresh_existing_templates_worktrees(by: str, job_id: int, group_settings: d dev: Device = session.query(Device).filter_by(hostname=hostname).one_or_none() if dev: dev.synchronized = False - add_sync_event(hostname, "refresh_templates", by, job_id) + add_sync_event(hostname, "refresh_templates", ret, job_id) updated_hostnames.add(hostname) if updated_hostnames: logger.debug( From 6e3d9ffb2ff2d269b70fab0f7c252725022f4b86 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Mon, 28 Oct 2024 13:53:46 +0100 Subject: [PATCH 10/14] break out parsing of what files in git changed into separate file and use it from git.py and git_worktrees.py for when worktree branches get updated --- src/cnaas_nms/db/git.py | 16 +++++----------- src/cnaas_nms/db/git_worktrees.py | 16 ++++++---------- src/cnaas_nms/tools/githelpers.py | 20 ++++++++++++++++++++ 3 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 src/cnaas_nms/tools/githelpers.py diff --git a/src/cnaas_nms/db/git.py b/src/cnaas_nms/db/git.py index 775fb4f5..bb0b39be 100644 --- a/src/cnaas_nms/db/git.py +++ b/src/cnaas_nms/db/git.py @@ -3,11 +3,12 @@ import json import os import shutil -from typing import Dict, Optional, Set, Tuple +from typing import Dict, List, Optional, Set, Tuple from urllib.parse import urldefrag import yaml +import git.remote from cnaas_nms.app_settings import app_settings from cnaas_nms.db.device import Device, DeviceType from cnaas_nms.db.exceptions import ConfigException, RepoStructureException @@ -27,6 +28,7 @@ from cnaas_nms.devicehandler.sync_history import add_sync_event from cnaas_nms.scheduler.thread_data import set_thread_data from cnaas_nms.tools.event import add_event +from cnaas_nms.tools.githelpers import parse_git_changed_files from cnaas_nms.tools.log import get_logger from git import InvalidGitRepositoryError, Repo from git.exc import GitCommandError, NoSuchPathError @@ -224,17 +226,9 @@ def _refresh_repo_task(repo_type: RepoType = RepoType.TEMPLATES, job_id: Optiona prev_commit = local_repo.commit().hexsha logger.debug("git pull from {}".format(remote_repo_path)) - diff = local_repo.remotes.origin.pull() - for item in diff: - if item.ref.remote_head != local_repo.head.ref.name: - continue + diff: List[git.remote.FetchInfo] = local_repo.remotes.origin.pull() + ret, changed_files = parse_git_changed_files(diff, prev_commit, local_repo) - ret += "Commit {} by {} at {}\n".format( - item.commit.name_rev, item.commit.committer, item.commit.committed_datetime - ) - diff_files = local_repo.git.diff("{}..{}".format(prev_commit, item.commit.hexsha), name_only=True).split() - changed_files.update(diff_files) - prev_commit = item.commit.hexsha except (InvalidGitRepositoryError, NoSuchPathError): # noqa: S110 logger.info("Local repository {} not found, cloning from remote".format(local_repo_path)) try: diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index 322ae2e3..1e4e036f 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -8,6 +8,7 @@ from cnaas_nms.db.groups import get_groups_using_branch from cnaas_nms.db.session import sqla_session from cnaas_nms.devicehandler.sync_history import add_sync_event +from cnaas_nms.tools.githelpers import parse_git_changed_files from cnaas_nms.tools.log import get_logger from git import Repo @@ -25,21 +26,16 @@ def refresh_existing_templates_worktrees(job_id: int, group_settings: dict, devi try: logger.info("Pulling worktree for branch {}".format(subdir)) wt_repo = Repo("/tmp/worktrees/" + subdir) + prev_commit = wt_repo.commit().hexsha diff = wt_repo.remotes.origin.pull() if not diff: continue - ret: str = "" - for item in diff: - # only check for changes in our branch - if item.ref.remote_head != wt_repo.head.ref.name: - continue - - ret += "Commit {} by {} at {}\n".format( - item.commit.name_rev, item.commit.committer, item.commit.committed_datetime - ) + ret: str + changed_files: Set[str] + ret, changed_files = parse_git_changed_files(diff, prev_commit, wt_repo) # don't update updated_groups if changes were only in other branches - if not ret: + if not changed_files: continue except Exception as e: logger.exception(e) diff --git a/src/cnaas_nms/tools/githelpers.py b/src/cnaas_nms/tools/githelpers.py new file mode 100644 index 00000000..3e988bb5 --- /dev/null +++ b/src/cnaas_nms/tools/githelpers.py @@ -0,0 +1,20 @@ +from typing import List, Set + +import git.remote +from git import Repo + + +def parse_git_changed_files(diff: List[git.remote.FetchInfo], prev_commit: str, local_repo: Repo) -> (str, Set[str]): + ret_msg = "" + changed_files: Set[str] = set() + for item in diff: + if item.ref.remote_head != local_repo.head.ref.name: + continue + + ret_msg += "Commit {} by {} at {}\n".format( + item.commit.name_rev, item.commit.committer, item.commit.committed_datetime + ) + diff_files = local_repo.git.diff("{}..{}".format(prev_commit, item.commit.hexsha), name_only=True).split() + changed_files.update(diff_files) + prev_commit = item.commit.hexsha + return ret_msg, changed_files From 7c969fe458e98ecc37f9ff93f65a287034293737 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Wed, 30 Oct 2024 14:22:17 +0100 Subject: [PATCH 11/14] Allow specifying unmanaged_config_sections in mapping.yml file which makes the config hash ignore those parts of the config This can be used together with get_config_section jinja filter to have sections of the configuration handled by an external tool --- src/cnaas_nms/db/device.py | 11 ++++++ src/cnaas_nms/db/git.py | 17 ++++++++- src/cnaas_nms/db/git_worktrees.py | 7 ++-- src/cnaas_nms/devicehandler/get.py | 29 +++++++++++++- src/cnaas_nms/devicehandler/sync_devices.py | 42 ++++++++++----------- 5 files changed, 80 insertions(+), 26 deletions(-) diff --git a/src/cnaas_nms/db/device.py b/src/cnaas_nms/db/device.py index b9cc7b9e..ac265ac9 100644 --- a/src/cnaas_nms/db/device.py +++ b/src/cnaas_nms/db/device.py @@ -7,6 +7,7 @@ import re from typing import List, Optional, Set +from nornir.core.inventory import Group as NornirGroup from sqlalchemy import Boolean, DateTime, Enum, ForeignKey, Integer, String, Unicode, UniqueConstraint, event from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy_utils import IPAddressType @@ -499,6 +500,16 @@ def validate(cls, new_entry=True, **kwargs): return data, errors + @classmethod + def nornir_groups_to_devicetype(cls, groups: List[NornirGroup]) -> DeviceType: + """Parse list of groups from nornir (task.host.groups) and return DeviceType""" + devtype: DeviceType = DeviceType.UNKNOWN + # Get the first group that starts with T_ and use that name to determine DeviceType + # Eg group name T_DIST -> DeviceType.DIST + devtype_name = next(filter(lambda x: x.name.startswith("T_"), groups)).name[2:] + devtype = DeviceType[devtype_name] + return devtype + @event.listens_for(Device, "after_update") def after_update_device(mapper, connection, target: Device): diff --git a/src/cnaas_nms/db/git.py b/src/cnaas_nms/db/git.py index bb0b39be..62f86296 100644 --- a/src/cnaas_nms/db/git.py +++ b/src/cnaas_nms/db/git.py @@ -12,7 +12,7 @@ from cnaas_nms.app_settings import app_settings from cnaas_nms.db.device import Device, DeviceType from cnaas_nms.db.exceptions import ConfigException, RepoStructureException -from cnaas_nms.db.git_worktrees import WorktreeError, refresh_existing_templates_worktrees +from cnaas_nms.db.git_worktrees import WorktreeError, find_templates_worktree_path, refresh_existing_templates_worktrees from cnaas_nms.db.job import Job, JobStatus from cnaas_nms.db.joblock import Joblock, JoblockError from cnaas_nms.db.session import redis_session, sqla_session @@ -22,6 +22,7 @@ VlanConflictError, get_device_primary_groups, get_group_settings_asdict, + get_group_templates_branch, get_groups, rebuild_settings_cache, ) @@ -409,3 +410,17 @@ def parse_repo_url(url: str) -> Tuple[str, Optional[str]]: """Parses a URL to a repository, returning the path and branch refspec separately""" path, branch = urldefrag(url) return path, branch if branch else None + + +def get_template_repo_path(hostname: str): + local_repo_path = app_settings.TEMPLATES_LOCAL + + # override template path if primary group template path is set + primary_group = get_device_primary_groups().get(hostname) + if primary_group: + templates_branch = get_group_templates_branch(primary_group) + if templates_branch: + primary_group_template_path = find_templates_worktree_path(templates_branch) + if primary_group_template_path: + local_repo_path = primary_group_template_path + return local_repo_path diff --git a/src/cnaas_nms/db/git_worktrees.py b/src/cnaas_nms/db/git_worktrees.py index 1e4e036f..0b2a3770 100644 --- a/src/cnaas_nms/db/git_worktrees.py +++ b/src/cnaas_nms/db/git_worktrees.py @@ -21,6 +21,7 @@ def refresh_existing_templates_worktrees(job_id: int, group_settings: dict, devi """Look for existing worktrees and refresh them""" logger = get_logger() updated_groups: Set[str] = set() + commit_by: str = "" if os.path.isdir("/tmp/worktrees"): for subdir in os.listdir("/tmp/worktrees"): try: @@ -31,9 +32,9 @@ def refresh_existing_templates_worktrees(job_id: int, group_settings: dict, devi if not diff: continue - ret: str changed_files: Set[str] - ret, changed_files = parse_git_changed_files(diff, prev_commit, wt_repo) + commit_by_new, changed_files = parse_git_changed_files(diff, prev_commit, wt_repo) + commit_by += commit_by_new # don't update updated_groups if changes were only in other branches if not changed_files: continue @@ -52,7 +53,7 @@ def refresh_existing_templates_worktrees(job_id: int, group_settings: dict, devi dev: Device = session.query(Device).filter_by(hostname=hostname).one_or_none() if dev: dev.synchronized = False - add_sync_event(hostname, "refresh_templates", ret, job_id) + add_sync_event(hostname, "refresh_templates", commit_by, job_id) updated_hostnames.add(hostname) if updated_hostnames: logger.debug( diff --git a/src/cnaas_nms/devicehandler/get.py b/src/cnaas_nms/devicehandler/get.py index b5a0f457..b669bef3 100644 --- a/src/cnaas_nms/devicehandler/get.py +++ b/src/cnaas_nms/devicehandler/get.py @@ -1,7 +1,9 @@ import hashlib +import os import re from typing import Dict, List, Optional +import yaml from netutils.config import compliance from netutils.lib_mapper import NAPALM_LIB_MAPPER from nornir.core.filter import F @@ -12,8 +14,11 @@ import cnaas_nms.devicehandler.nornir_helper from cnaas_nms.db.device import Device, DeviceType from cnaas_nms.db.device_vars import expand_interface_settings +from cnaas_nms.db.exceptions import RepoStructureException +from cnaas_nms.db.git import get_template_repo_path from cnaas_nms.db.interface import Interface, InterfaceConfigType, InterfaceError from cnaas_nms.db.session import sqla_session +from cnaas_nms.tools.jinja_filters import get_config_section from cnaas_nms.tools.log import get_logger @@ -53,7 +58,29 @@ def get_running_config_interface(session: sqla_session, hostname: str, interface return "\n".join(ret) -def calc_config_hash(hostname, config): +def get_unmanaged_config_sections(hostname: str, platform: str, devtype: DeviceType) -> List[str]: + local_repo_path = get_template_repo_path(hostname) + + mapfile = os.path.join(local_repo_path, platform, "mapping.yml") + if not os.path.isfile(mapfile): + raise RepoStructureException("File {} not found in template repo".format(mapfile)) + with open(mapfile, "r") as f: + mapping = yaml.safe_load(f) + if ( + "unmanaged_config_sections" in mapping[devtype.name] + and type(mapping[devtype.name]["unmanaged_config_sections"]) is list + ): + return mapping[devtype.name]["unmanaged_config_sections"] + return [] + + +def calc_config_hash(hostname: str, config: str, platform: str, devtype: DeviceType): + ignore_config_sections: List[str] = get_unmanaged_config_sections(hostname, platform, devtype) + for section in ignore_config_sections: + skip_section = get_config_section(config, section, platform) + if skip_section: + config = config.replace(skip_section, "") + config = config.replace("\n", "") try: hash_object = hashlib.sha256(config.encode()) except Exception: diff --git a/src/cnaas_nms/devicehandler/sync_devices.py b/src/cnaas_nms/devicehandler/sync_devices.py index e3d8e493..2d4083c1 100644 --- a/src/cnaas_nms/devicehandler/sync_devices.py +++ b/src/cnaas_nms/devicehandler/sync_devices.py @@ -1,7 +1,6 @@ import datetime import os import time -from hashlib import sha256 from ipaddress import IPv4Address, IPv4Interface, ip_interface from typing import List, Optional, Tuple @@ -15,16 +14,15 @@ from nornir_utils.plugins.functions import print_result import cnaas_nms.db.helper -from cnaas_nms.app_settings import api_settings, app_settings +from cnaas_nms.app_settings import api_settings from cnaas_nms.db.device import Device, DeviceState, DeviceType from cnaas_nms.db.device_vars import expand_interface_settings -from cnaas_nms.db.git import RepoStructureException -from cnaas_nms.db.git_worktrees import find_templates_worktree_path +from cnaas_nms.db.git import RepoStructureException, get_template_repo_path from cnaas_nms.db.interface import Interface from cnaas_nms.db.job import Job from cnaas_nms.db.joblock import Joblock, JoblockError from cnaas_nms.db.session import redis_session, sqla_session -from cnaas_nms.db.settings import get_device_primary_groups, get_group_templates_branch, get_settings +from cnaas_nms.db.settings import get_settings from cnaas_nms.devicehandler.changescore import calculate_score from cnaas_nms.devicehandler.get import calc_config_hash from cnaas_nms.devicehandler.nornir_helper import NornirJobResult, cnaas_init, get_jinja_env, inventory_selector @@ -532,16 +530,7 @@ def push_sync_device( platform = dev.platform devtype = dev.device_type - local_repo_path = app_settings.TEMPLATES_LOCAL - - # override template path if primary group template path is set - primary_group = get_device_primary_groups().get(hostname) - if primary_group: - templates_branch = get_group_templates_branch(primary_group) - if templates_branch: - primary_group_template_path = find_templates_worktree_path(templates_branch) - if primary_group_template_path: - local_repo_path = primary_group_template_path + local_repo_path = get_template_repo_path(hostname) mapfile = os.path.join(local_repo_path, platform, "mapping.yml") if not os.path.isfile(mapfile): @@ -674,6 +663,7 @@ def sync_check_hash(task, force=False, job_id=None): task: Nornir task force: Ignore device hash """ + logger = get_logger() set_thread_data(job_id) if force is True: return @@ -686,11 +676,12 @@ def sync_check_hash(task, force=False, job_id=None): res = task.run(task=napalm_get, getters=["config"]) task.host.close_connection("napalm") - running_config = dict(res.result)["config"]["running"].encode() - if running_config is None: - raise Exception("Failed to get running configuration") - hash_obj = sha256(running_config) - running_hash = hash_obj.hexdigest() + try: + devtype = Device.nornir_groups_to_devicetype(task.host.groups) + except Exception as e: + logger.error("Unable to determine device type") + logger.exception(e) + running_hash = calc_config_hash(task.host.name, dict(res.result)["config"]["running"], task.host.platform, devtype) if stored_hash != running_hash: raise Exception("Device {} configuration is altered outside of CNaaS!".format(task.host.name)) @@ -706,7 +697,16 @@ def update_config_hash(task): or "config" not in res[0].result ): raise Exception("Unable to get config from device") - new_config_hash = calc_config_hash(task.host.name, res[0].result["config"]["running"]) + + try: + devtype = Device.nornir_groups_to_devicetype(task.host.groups) + except Exception as e: + logger.error("Unable to determine device type") + logger.exception(e) + + new_config_hash = calc_config_hash( + task.host.name, res[0].result["config"]["running"], task.host.platform, devtype + ) if not new_config_hash: raise ValueError("Empty config hash") except Exception as e: From 35f44c0cc3e9fc365450c556c70feb30a68cf2ef Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Wed, 30 Oct 2024 14:36:41 +0100 Subject: [PATCH 12/14] Use unmanaged_config_sections to determine if we need to get running_config for platform/devtype combo, this way we don't need a new config variable that needs to be synced with templates --- src/cnaas_nms/app_settings.py | 4 +--- src/cnaas_nms/devicehandler/sync_devices.py | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/cnaas_nms/app_settings.py b/src/cnaas_nms/app_settings.py index 6bd9f1af..ce64b238 100644 --- a/src/cnaas_nms/app_settings.py +++ b/src/cnaas_nms/app_settings.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import List, Optional +from typing import Optional import yaml from pydantic import field_validator @@ -57,7 +57,6 @@ class ApiSettings(BaseSettings): COMMIT_CONFIRMED_TIMEOUT: int = 300 COMMIT_CONFIRMED_WAIT: int = 1 SETTINGS_OVERRIDE: Optional[dict] = None - DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG: List[str] = [] @field_validator("MGMTDOMAIN_PRIMARY_IP_VERSION") @classmethod @@ -119,7 +118,6 @@ def construct_api_settings() -> ApiSettings: COMMIT_CONFIRMED_TIMEOUT=config.get("commit_confirmed_timeout", 300), COMMIT_CONFIRMED_WAIT=config.get("commit_confirmed_wait", 1), SETTINGS_OVERRIDE=config.get("settings_override", None), - DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG=config.get("device_types_with_include_running_config", []), ) else: return ApiSettings() diff --git a/src/cnaas_nms/devicehandler/sync_devices.py b/src/cnaas_nms/devicehandler/sync_devices.py index 2d4083c1..51603910 100644 --- a/src/cnaas_nms/devicehandler/sync_devices.py +++ b/src/cnaas_nms/devicehandler/sync_devices.py @@ -358,15 +358,23 @@ def populate_device_vars( ) device_variables = {**device_variables, **fabric_device_variables} - # if device type in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG - for dt_str in api_settings.DEVICE_TYPES_WITH_INCLUDE_RUNNING_CONFIG: - if dev.device_type.name.lower() == dt_str.lower(): + # if platform/devtype has unmanaged config sections, get running_config and add to device_variables + local_repo_path = get_template_repo_path(hostname) + mapfile = os.path.join(local_repo_path, dev.platform, "mapping.yml") + if not os.path.isfile(mapfile): + raise RepoStructureException("File {} not found in template repo".format(mapfile)) + with open(mapfile, "r") as f: + mapping = yaml.safe_load(f) + if ( + "unmanaged_config_sections" in mapping[devtype.name] + and type(mapping[devtype.name]["unmanaged_config_sections"]) is list + ): task.host.open_connection("napalm", configuration=task.nornir.config) res = task.run(task=napalm_get, getters=["config"]) task.host.close_connection("napalm") running_config = dict(res.result)["config"]["running"] - # Remove the first task result, which is the napalm_get result, since it's not needed anymore + # Remove the first task result, which is the napalm_get result, since it's not needed for final job result del task.results[0] if running_config is None: raise Exception(f"Failed to get running configuration for {dev.hostname}") From 052977d58bb445ae9f38167cc5641cbfcb289c88 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Thu, 31 Oct 2024 09:38:03 +0100 Subject: [PATCH 13/14] Fix for junos where first line of config includes "## Last commit " comment that make config hash diff, even if the config change was only in unmanaged parts of the config --- src/cnaas_nms/devicehandler/get.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/cnaas_nms/devicehandler/get.py b/src/cnaas_nms/devicehandler/get.py index b669bef3..d5781b51 100644 --- a/src/cnaas_nms/devicehandler/get.py +++ b/src/cnaas_nms/devicehandler/get.py @@ -80,6 +80,9 @@ def calc_config_hash(hostname: str, config: str, platform: str, devtype: DeviceT skip_section = get_config_section(config, section, platform) if skip_section: config = config.replace(skip_section, "") + if platform == "junos": + # remove line starting with "## Last commit" from config string so we don't get config hash mismatch + config = re.sub(r"^#{2}.*\n", "", config, flags=re.MULTILINE) config = config.replace("\n", "") try: hash_object = hashlib.sha256(config.encode()) From 41385b4143cc5ebd1ebac9fdebc051960322a323 Mon Sep 17 00:00:00 2001 From: Johan Marcusson Date: Thu, 31 Oct 2024 09:55:43 +0100 Subject: [PATCH 14/14] Allow increasing of napalm timeout (for slow firewall clusters) --- docs/configuration/index.rst | 2 ++ src/cnaas_nms/app_settings.py | 2 ++ .../devicehandler/nornir_plugins/cnaas_inventory.py | 5 +++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index 3c505f4c..838bb398 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -47,6 +47,8 @@ Defines parameters for the API: specified in seconds. Defaults to 300. - commit_confirmed_wait: Time to wait between comitting configuration and checking that the device is still reachable, specified in seconds. Defaults to 1. +- napalm_timeout: Timeout for NAPALM operations, specified in seconds. Defaults to 60. + Increase if you get errors like "jnpr.junos.exception.RpcTimeoutError: RpcTimeoutError" on jobs. /etc/cnaas-nms/auth_config.yml ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/src/cnaas_nms/app_settings.py b/src/cnaas_nms/app_settings.py index ce64b238..978a890a 100644 --- a/src/cnaas_nms/app_settings.py +++ b/src/cnaas_nms/app_settings.py @@ -57,6 +57,7 @@ class ApiSettings(BaseSettings): COMMIT_CONFIRMED_TIMEOUT: int = 300 COMMIT_CONFIRMED_WAIT: int = 1 SETTINGS_OVERRIDE: Optional[dict] = None + NAPALM_TIMEOUT: int = 60 @field_validator("MGMTDOMAIN_PRIMARY_IP_VERSION") @classmethod @@ -118,6 +119,7 @@ def construct_api_settings() -> ApiSettings: COMMIT_CONFIRMED_TIMEOUT=config.get("commit_confirmed_timeout", 300), COMMIT_CONFIRMED_WAIT=config.get("commit_confirmed_wait", 1), SETTINGS_OVERRIDE=config.get("settings_override", None), + NAPALM_TIMEOUT=config.get("napalm_timeout", 60), ) else: return ApiSettings() diff --git a/src/cnaas_nms/devicehandler/nornir_plugins/cnaas_inventory.py b/src/cnaas_nms/devicehandler/nornir_plugins/cnaas_inventory.py index c265c386..84e2046f 100644 --- a/src/cnaas_nms/devicehandler/nornir_plugins/cnaas_inventory.py +++ b/src/cnaas_nms/devicehandler/nornir_plugins/cnaas_inventory.py @@ -3,7 +3,7 @@ from nornir.core.inventory import ConnectionOptions, Defaults, Group, Groups, Host, Hosts, Inventory, ParentGroups import cnaas_nms.db.session -from cnaas_nms.app_settings import app_settings +from cnaas_nms.app_settings import api_settings, app_settings from cnaas_nms.db.device import Device, DeviceState, DeviceType from cnaas_nms.db.settings import get_groups from cnaas_nms.tools.pki import ssl_context @@ -41,11 +41,12 @@ def load(self) -> Inventory: connection_options={ "napalm": ConnectionOptions( extras={ + "timeout": api_settings.NAPALM_TIMEOUT, "optional_args": { # args to eAPI HttpsEapiConnection for EOS "enforce_verification": True, "context": ssl_context, - } + }, } ), "netmiko": ConnectionOptions(extras={}),