@@ -91,154 +91,181 @@ def _get_commands_to_run(yaml_parsed_info, skip_list=None):
9191 return deduplicate_command_list (all_commands )
9292
9393
94- def netmiko_send_commands (task : Task ,
95- command_getter_yaml_data : Dict ,
96- command_getter_job : str ,
97- logger ,
98- nautobot_job
99- ):
100- """Run commands specified in PLATFORM_COMMAND_MAP."""
101- if not task .host .platform :
102- return Result (host = task .host , result = f"{ task .host .name } has no platform set." , failed = True )
103- if task .host .platform not in get_all_network_driver_mappings ().keys () or not "cisco_wlc_ssh" :
104- return Result (host = task .host , result = f"{ task .host .name } has a unsupported platform set." , failed = True )
105- if not command_getter_yaml_data [task .host .platform ].get (command_getter_job ):
94+ def netmiko_send_commands (
95+ task : Task ,
96+ command_getter_yaml_data : Dict ,
97+ command_getter_job : str ,
98+ logger ,
99+ nautobot_job ,
100+ ** kwargs ,
101+ ):
102+ """Run platform-specific commands with optional parsing and logging."""
103+
104+ command_exclusions = kwargs .get ("command_exclusions" , [])
105+ connectivity_test = kwargs .get ("connectivity_test" , False )
106+ # sync_cables = kwargs.get("sync_cables", False)
107+
108+ # ---- 1. Validation -------------------------------------------------------
109+ validation_result = _validate_task (task , command_getter_yaml_data , command_getter_job )
110+ if validation_result :
111+ return validation_result
112+
113+ if connectivity_test and not _check_connectivity (task ):
106114 return Result (
107- host = task .host , result = f"{ task .host .name } has missing definitions in command_mapper YAML file." , failed = True
115+ host = task .host ,
116+ result = f"{ task .host .name } failed connectivity check via tcp_ping." ,
117+ failed = True ,
108118 )
109- if nautobot_job .connectivity_test :
110- if not tcp_ping (task .host .hostname , task .host .port ):
111- return Result (
112- host = task .host , result = f"{ task .host .name } failed connectivity check via tcp_ping." , failed = True
113- )
114- task .host .data ["platform_parsing_info" ] = command_getter_yaml_data [task .host .platform ]
115119
116- skip_conditions = {
117- "interfaces__tagged_vlans" : not sync_vlans ,
118- "vlan_map" : not sync_vlans ,
119- "interfaces__untagged_vlan" : not sync_vlans ,
120- "interfaces__vrf" : not sync_vrfs ,
121- "cables" : not sync_cables ,
122- "software_version" : not sync_software_version ,
123- }
120+ task .host .data ["platform_parsing_info" ] = command_getter_yaml_data [task .host .platform ]
124121
122+ # ---- 2. Command Preparation ---------------------------------------------
125123 commands = _get_commands_to_run (
126- command_getter_yaml_data [task .host .platform ][command_getter_job ],
127- # getattr(nautobot_job, "sync_vlans", False),
128- # getattr(nautobot_job, "sync_vrfs", False),
129- # getattr(nautobot_job, "sync_cables", False),
130- # getattr(nautobot_job, "sync_software_version", False),
124+ yaml_parsed_info = command_getter_yaml_data [task .host .platform ][command_getter_job ],
125+ skip_list = command_exclusions ,
131126 )
127+ logger .debug (f"Commands to run: { [cmd ['command' ] for cmd in commands ]} " )
128+
129+ # ---- 3. Command Execution & Parsing -------------------------------------
130+ for idx , command in enumerate (commands ):
131+ try :
132+ current_result = _run_command (task , command )
133+ _handle_command_result (task , idx , command , current_result , nautobot_job , logger )
134+ except NornirSubTaskError as e :
135+ return _handle_subtask_error (task , idx , e )
136+
137+ return Result (host = task .host , result = "Commands executed successfully." , failed = False )
138+
139+
140+ # -----------------------------------------------------------------------------
141+ # Helper functions
142+ # -----------------------------------------------------------------------------
143+
144+ def _validate_task (task , yaml_data , job ):
145+ """Check platform and YAML definition validity."""
146+ platform = task .host .platform
147+ if not platform :
148+ return Result (host = task .host , result = f"{ task .host .name } has no platform set." , failed = True )
149+
150+ supported_platforms = get_all_network_driver_mappings ().keys ()
151+ if platform not in supported_platforms and platform != "cisco_wlc_ssh" :
152+ return Result (host = task .host , result = f"{ task .host .name } has an unsupported platform." , failed = True )
132153
133- if (
134- getattr (nautobot_job , "sync_cables" , False )
135- and "cables" not in command_getter_yaml_data [task .host .platform ][command_getter_job ].keys ()
136- ):
137- logger .error (
138- f"{ task .host .platform } has missing definitions for cables in command_mapper YAML file. Cables will not be loaded."
154+ if not yaml_data .get (platform , {}).get (job ):
155+ return Result (
156+ host = task .host ,
157+ result = f"{ task .host .name } missing definitions in command_mapper YAML." ,
158+ failed = True ,
139159 )
140160
141- logger .debug (f"Commands to run: { [cmd ['command' ] for cmd in commands ]} " )
142- # All commands in this for loop are running within 1 device connection.
143- for result_idx , command in enumerate (commands ):
144- send_command_kwargs = {}
161+ return None
162+
163+
164+ def _check_connectivity (task ):
165+ """Perform TCP ping check."""
166+ return tcp_ping (task .host .hostname , task .host .port )
167+
168+
169+ def _run_command (task , command ):
170+ """Execute a single Netmiko command."""
171+ return task .run (
172+ task = netmiko_send_command ,
173+ name = command ["command" ],
174+ command_string = command ["command" ],
175+ read_timeout = 60 ,
176+ )
177+
178+
179+ def _handle_command_result (task , idx , command , current_result , nautobot_job , logger ):
180+ """Parse and store results based on parser type."""
181+ raw_output = current_result .result
182+ parser_type = command .get ("parser" )
183+
184+ # Debug output
185+ if nautobot_job .debug :
186+ log_message = format_log_message (pprint .pformat (raw_output ))
187+ logger .debug (f"Result of '{ command ['command' ]} ' command:<br><br>{ log_message } " )
188+
189+ # Handle invalid input gracefully
190+ if isinstance (raw_output , str ) and "Invalid input detected" in raw_output :
191+ task .results [idx ].result = []
192+ task .results [idx ].failed = False
193+ return
194+
195+ if parser_type in SUPPORTED_COMMAND_PARSERS :
196+ parsed = _parse_command_output (task , command , raw_output , parser_type , nautobot_job , logger )
197+ task .results [idx ].result = parsed
198+ task .results [idx ].failed = False
199+ else :
200+ task .results [idx ].result = _handle_raw_or_none (raw_output , parser_type )
201+ task .results [idx ].failed = False
202+
203+
204+ def _parse_command_output (task , command , raw_output , parser_type , nautobot_job , logger ):
205+ """Dispatch to the appropriate parser."""
206+ try :
207+ if parser_type == "textfsm" :
208+ return _parse_textfsm (task , command , raw_output , nautobot_job , logger )
209+ elif parser_type == "ttp" :
210+ return _parse_ttp (task , command , raw_output )
211+ except Exception as e :
212+ logger .warning (f"Parsing failed for { command ['command' ]} : { e } " )
213+ return []
214+ return []
215+
216+
217+ def _parse_textfsm (task , command , data , nautobot_job , logger ):
218+ git_template_dir = get_git_repo_parser_path ("textfsm" )
219+ if git_template_dir and not check_for_required_file (git_template_dir , "index" ):
220+ logger .debug (f"Missing index file in { git_template_dir } , falling back to defaults." )
221+ git_template_dir = None
222+
223+ parsed_output = parse_output (
224+ platform = get_all_network_driver_mappings ()[task .host .platform ]["ntc_templates" ],
225+ template_dir = git_template_dir ,
226+ command = command ["command" ],
227+ data = data ,
228+ try_fallback = bool (git_template_dir ),
229+ )
230+
231+ if nautobot_job .debug :
232+ logger .debug (format_log_message (pprint .pformat (parsed_output )))
233+
234+ return parsed_output
235+
236+
237+ def _parse_ttp (task , command , data ):
238+ ttp_template_files = load_files_with_precedence (f"{ PARSER_DIR } /ttp" , "ttp" )
239+ template_name = f"{ task .host .platform } _{ command ['command' ].replace (' ' , '_' )} .ttp"
240+ parser = ttp (data = data , template = ttp_template_files [template_name ])
241+ parser .parse ()
242+ return json .loads (parser .result (format = "json" )[0 ])
243+
244+
245+ def _handle_raw_or_none (raw_output , parser_type ):
246+ if parser_type == "raw" :
247+ return {"raw" : raw_output }
248+ if parser_type == "none" :
145249 try :
146- current_result = task .run (
147- task = netmiko_send_command ,
148- name = command ["command" ],
149- command_string = command ["command" ],
150- read_timeout = 60 ,
151- ** send_command_kwargs ,
152- )
153- if nautobot_job .debug :
154- log_message = format_log_message (pprint .pformat (current_result .result ))
155- logger .debug (f"Result of '{ command ['command' ]} ' command:<br><br>{ log_message } " )
156- if command .get ("parser" ) in SUPPORTED_COMMAND_PARSERS :
157- if isinstance (current_result .result , str ):
158- if "Invalid input detected at" in current_result .result :
159- task .results [result_idx ].result = []
160- task .results [result_idx ].failed = False
161- else :
162- if command ["parser" ] == "textfsm" :
163- try :
164- # Look for custom textfsm templates in the git repo
165- git_template_dir = get_git_repo_parser_path (parser_type = "textfsm" )
166- if git_template_dir :
167- if not check_for_required_file (git_template_dir , "index" ):
168- logger .debug (
169- f"Unable to find required index file in { git_template_dir } for textfsm parsing. Falling back to default templates."
170- )
171- git_template_dir = None
172- # Parsing textfsm ourselves instead of using netmikos use_<parser> function to be able to handle exceptions
173- # ourselves. Default for netmiko is if it can't parse to return raw text which is tougher to handle.
174- parsed_output = parse_output (
175- platform = get_all_network_driver_mappings ()[task .host .platform ]["ntc_templates" ],
176- template_dir = git_template_dir if git_template_dir else None ,
177- command = command ["command" ],
178- data = current_result .result ,
179- try_fallback = bool (git_template_dir ),
180- )
181- if nautobot_job .debug :
182- log_message = format_log_message (pprint .pformat (parsed_output ))
183- logger .debug (
184- f"Parsed output of '{ command ['command' ]} ' command:<br><br>{ log_message } "
185- )
186- task .results [result_idx ].result = parsed_output
187- task .results [result_idx ].failed = False
188- except Exception : # https://github.com/networktocode/ntc-templates/issues/369
189- try :
190- if nautobot_job .debug :
191- traceback_str = traceback .format_exc ().replace ("\n " , "<br>" )
192- logger .warning (
193- f"Parsing failed for '{ command ['command' ]} ' command:<br><br>{ traceback_str } "
194- )
195- except : # noqa: E722, S110
196- pass
197- task .results [result_idx ].result = []
198- task .results [result_idx ].failed = False
199- if command ["parser" ] == "ttp" :
200- try :
201- # Parsing ttp ourselves instead of using netmikos use_<parser> function to be able to handle exceptions
202- # ourselves.
203- ttp_template_files = load_files_with_precedence (
204- filesystem_dir = f"{ PARSER_DIR } /ttp" , parser_type = "ttp"
205- )
206- template_name = f"{ task .host .platform } _{ command ['command' ].replace (' ' , '_' )} .ttp"
207- parser = ttp (
208- data = current_result .result ,
209- template = ttp_template_files [template_name ],
210- )
211- parser .parse ()
212- parsed_result = parser .result (format = "json" )[0 ]
213- # task.results[result_idx].result = json.loads(json.dumps(parsed_result))
214- task .results [result_idx ].result = json .loads (parsed_result )
215- task .results [result_idx ].failed = False
216- except Exception :
217- task .results [result_idx ].result = []
218- task .results [result_idx ].failed = False
219- else :
220- if command ["parser" ] == "raw" :
221- raw = {"raw" : current_result .result }
222- task .results [result_idx ].result = json .loads (json .dumps (raw ))
223- task .results [result_idx ].failed = False
224- if command ["parser" ] == "none" :
225- try :
226- jsonified = json .loads (current_result .result )
227- task .results [result_idx ].result = jsonified
228- task .results [result_idx ].failed = False
229- except Exception :
230- task .result .failed = False
231- except NornirSubTaskError :
232- # These exceptions indicate that the device is unreachable or the credentials are incorrect.
233- # We should fail the task early to avoid trying all commands on a device that is unreachable.
234- if type (task .results [result_idx ].exception ).__name__ == "NetmikoAuthenticationException" :
235- return Result (host = task .host , result = f"{ task .host .name } failed authentication." , failed = True )
236- if type (task .results [result_idx ].exception ).__name__ == "NetmikoTimeoutException" :
237- return Result (host = task .host , result = f"{ task .host .name } SSH Timeout Occured." , failed = True )
238- # We don't want to fail the entire subtask if SubTaskError is hit, set result to empty list and failed to False
239- # Handle this type or result latter in the ETL process.
240- task .results [result_idx ].result = []
241- task .results [result_idx ].failed = False
250+ return json .loads (raw_output )
251+ except Exception :
252+ return []
253+ return []
254+
255+
256+ def _handle_subtask_error (task , idx , exception ):
257+ """Handle connection/authentication errors gracefully."""
258+ exc_type = type (task .results [idx ].exception ).__name__
259+
260+ if exc_type == "NetmikoAuthenticationException" :
261+ return Result (host = task .host , result = f"{ task .host .name } failed authentication." , failed = True )
262+ if exc_type == "NetmikoTimeoutException" :
263+ return Result (host = task .host , result = f"{ task .host .name } SSH timeout occurred." , failed = True )
264+
265+ task .results [idx ].result = []
266+ task .results [idx ].failed = False
267+
268+ return None
242269
243270
244271@lru_cache (maxsize = None )
@@ -309,12 +336,14 @@ def sync_devices_command_getter(job, log_level):
309336 )
310337 continue
311338 nr_with_processors .inventory .hosts .update (single_host_inventory_constructed )
339+
312340 nr_with_processors .run (
313341 task = netmiko_send_commands ,
314342 command_getter_yaml_data = nr_with_processors .inventory .defaults .data ["platform_parsing_info" ],
315343 command_getter_job = "sync_devices" ,
316344 logger = logger ,
317345 nautobot_job = job ,
346+ # command_getter_yaml_exclusions=command_exclusions,
318347 )
319348 except Exception as err : # pylint: disable=broad-exception-caught
320349 try :
@@ -348,21 +377,44 @@ def sync_network_data_command_getter(job, log_level):
348377 "defaults" : {
349378 "platform_parsing_info" : add_platform_parsing_info (),
350379 "network_driver_mappings" : list (get_all_network_driver_mappings ().keys ()),
351- "sync_vlans" : job .sync_vlans ,
352- "sync_vrfs" : job .sync_vrfs ,
353- "sync_cables" : job .sync_cables ,
354- "sync_software_version" : job .sync_software_version ,
380+ # "sync_vlans": job.sync_vlans,
381+ # "sync_vrfs": job.sync_vrfs,
382+ # "sync_cables": job.sync_cables,
383+ # "sync_software_version": job.sync_software_version,
355384 },
356385 },
357386 },
358387 ) as nornir_obj :
388+
389+ command_exclusions = {
390+ "interfaces__tagged_vlans" : not job .sync_vlans ,
391+ "vlan_map" : not job .sync_vlans ,
392+ "interfaces__untagged_vlan" : not job .sync_vlans ,
393+ "interfaces__vrf" : not job .sync_vrfs ,
394+ "cables" : not job .sync_cables ,
395+ "software_version" : not job .sync_software_version ,
396+ }
397+ exclusions = [k for k , v in command_exclusions .items () if v ]
398+
399+ # commands = _get_commands_to_run(
400+ # command_getter_yaml_data[task.host.platform][command_getter_job],
401+ # # getattr(nautobot_job, "sync_vlans", False),
402+ # # getattr(nautobot_job, "sync_vrfs", False),
403+ # # getattr(nautobot_job, "sync_cables", False),
404+ # # getattr(nautobot_job, "sync_software_version", False),
405+ # )
406+
407+
359408 nr_with_processors = nornir_obj .with_processors ([CommandGetterProcessor (logger , compiled_results , job )])
360409 nr_with_processors .run (
361410 task = netmiko_send_commands ,
362411 command_getter_yaml_data = nr_with_processors .inventory .defaults .data ["platform_parsing_info" ],
363412 command_getter_job = "sync_network_data" ,
364413 logger = logger ,
365- nautobot_job = job ,
414+ command_exclusions = exclusions ,
415+ connectivity_test = job .connectivity_test ,
416+ sync_cables = job .sync_cables ,
417+ # nautobot_job=job,
366418 )
367419 except Exception : # pylint: disable=broad-exception-caught
368420 logger .info (f"Error During Sync Network Data Command Getter: { traceback .format_exc ()} " )
0 commit comments