Skip to content

Commit

Permalink
Overhaul on netmiko cli-tools output formatting behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
ktbyers authored Oct 1, 2024
1 parent 78d8c11 commit 3e0623e
Show file tree
Hide file tree
Showing 14 changed files with 851 additions and 607 deletions.
119 changes: 119 additions & 0 deletions netmiko/cli_tools/argument_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import argparse
from getpass import getpass
from netmiko.utilities import load_devices, display_inventory


def common_args(parser):
"""Add common arguments to the parser."""
parser.add_argument(
"--cmd",
help="Command to execute",
action="store",
default=None,
type=str,
)
parser.add_argument("--username", help="Username", action="store", type=str)
parser.add_argument("--password", help="Password", action="store_true")
parser.add_argument("--secret", help="Enable Secret", action="store_true")
parser.add_argument(
"--list-devices", help="List devices from inventory", action="store_true"
)
parser.add_argument(
"--display-runtime", help="Display program runtime", action="store_true"
)
parser.add_argument(
"--hide-failed", help="Hide failed devices", action="store_true"
)
parser.add_argument(
"--json", help="Output results in JSON format", action="store_true"
)
parser.add_argument("--raw", help="Display raw output", action="store_true")
parser.add_argument("--version", help="Display version", action="store_true")


def show_args(parser):
"""Add arguments specific to netmiko_show.py."""
parser.add_argument(
"devices",
help="Device or group to connect to",
action="store",
type=str,
)


def cfg_args(parser):
"""Add arguments specific to netmiko_cfg.py."""
parser.add_argument(
"devices",
help="Device or group to connect to",
action="store",
type=str,
)
parser.add_argument(
"--infile", help="Read commands from file", type=argparse.FileType("r")
)


def grep_args(parser):
"""Add arguments specific to netmiko_grep.py."""
parser.add_argument(
"pattern", nargs="?", help="Pattern to search for", action="store", type=str
)
parser.add_argument(
"devices",
help="Device or group to connect to",
action="store",
type=str,
)


def parse_arguments(args, command):
"""Parse command-line arguments for all scripts."""

if command == "netmiko-cfg":
description = "Execute configurations command using Netmiko"
addl_args = cfg_args
elif command == "netmiko-show":
description = "Execute show command using Netmiko (defaults to 'show run')"
addl_args = show_args
elif command == "netmiko-grep":
description = "Grep pattern search on Netmiko output (defaults to 'show run')"
addl_args = grep_args
else:
raise ValueError(f"Unknown Netmiko cli-tool: {command}")

parser = argparse.ArgumentParser(description=description)
common_args(parser)

# Add additional arguments based (addl_args references a function)
addl_args(parser)

cli_args = parser.parse_args(args)
if not cli_args.list_devices and not cli_args.version:
if not cli_args.devices:
parser.error("Devices not specified.")
elif command == "netmiko-cfg" and not cli_args.cmd and not cli_args.infile:
parser.error("No configuration commands provided.")
elif command == "netmiko-grep" and not cli_args.pattern:
parser.error("Grep pattern not specified.")

return cli_args


def extract_cli_vars(cli_args, command, __version__):

return_vars = {}
return_vars["cli_username"] = cli_args.username if cli_args.username else None
return_vars["cli_password"] = getpass() if cli_args.password else None
return_vars["cli_secret"] = getpass("Enable secret: ") if cli_args.secret else None
version = cli_args.version
if version:
print(f"{command} v{__version__}")
return 0
list_devices = cli_args.list_devices
if list_devices:
my_devices = load_devices()
display_inventory(my_devices)
return 0

return return_vars
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Any, Dict

from netmiko import ConnectHandler
from netmiko.utilities import load_devices, obtain_all_devices
from netmiko.cli_tools import ERROR_PATTERN
Expand Down
150 changes: 39 additions & 111 deletions netmiko/cli_tools/netmiko_cfg.py
Original file line number Diff line number Diff line change
@@ -1,81 +1,16 @@
#!/usr/bin/env python
"""Return output from single show cmd using Netmiko."""
import argparse
import sys
import os
import subprocess
from datetime import datetime
from getpass import getpass
from concurrent.futures import ThreadPoolExecutor, as_completed

from netmiko.utilities import load_devices, display_inventory
from netmiko.utilities import write_tmp_file, ensure_dir_exists
from netmiko.utilities import find_netmiko_dir
from netmiko.cli_tools import ERROR_PATTERN, GREP, MAX_WORKERS, __version__
from netmiko.cli_tools.cli_helpers import obtain_devices, update_device_params, ssh_conn


def grepx(files, pattern, grep_options, use_colors=True):
"""Call system grep"""
if not isinstance(files, (list, tuple)):
files = [files]
if use_colors:
grep_options += ["--color=auto"]

# Make grep output look nicer by 'cd netmiko_full_dir'
_, netmiko_full_dir = find_netmiko_dir()
os.chdir(netmiko_full_dir)
# Convert files to strip off the directory
retrieve_file = lambda x: x.split("/")[-1] # noqa
files = [retrieve_file(a_file) for a_file in files]
files.sort()
grep_list = [GREP] + grep_options + [pattern] + files
proc = subprocess.Popen(grep_list, shell=False)
proc.communicate()
return ""


def parse_arguments(args):
"""Parse command-line arguments."""
description = "Execute single config cmd using Netmiko"
parser = argparse.ArgumentParser(description=description)
parser.add_argument(
"devices",
nargs="?",
help="Device or group to connect to",
action="store",
type=str,
)
parser.add_argument(
"--infile", help="Read commands from file", type=argparse.FileType("r")
)
parser.add_argument(
"--cmd",
help="Config command to execute",
action="store",
default=None,
type=str,
)
parser.add_argument("--username", help="Username", action="store", type=str)
parser.add_argument("--password", help="Password", action="store_true")
parser.add_argument("--secret", help="Enable Secret", action="store_true")
parser.add_argument(
"--list-devices", help="List devices from inventory", action="store_true"
)
parser.add_argument(
"--display-runtime", help="Display program runtime", action="store_true"
)
parser.add_argument(
"--hide-failed", help="Hide failed devices", action="store_true"
)
parser.add_argument("--version", help="Display version", action="store_true")
cli_args = parser.parse_args(args)
if not cli_args.list_devices and not cli_args.version:
if not cli_args.devices:
parser.error("Devices not specified.")
if not cli_args.cmd and not cli_args.infile:
parser.error("No configuration commands provided.")
return cli_args
from netmiko.cli_tools import ERROR_PATTERN, MAX_WORKERS, __version__
from netmiko.cli_tools.helpers import obtain_devices, update_device_params, ssh_conn
from netmiko.cli_tools.outputters import output_dispatcher, output_failed_devices
from netmiko.cli_tools.argument_handling import parse_arguments, extract_cli_vars


COMMAND = "netmiko-cfg"


def main_ep():
Expand All @@ -84,22 +19,14 @@ def main_ep():

def main(args):
start_time = datetime.now()
cli_args = parse_arguments(args)

cli_username = cli_args.username if cli_args.username else None
cli_password = getpass() if cli_args.password else None
cli_secret = getpass("Enable secret: ") if cli_args.secret else None

version = cli_args.version
if version:
print("netmiko-cfg v{}".format(__version__))
return 0
list_devices = cli_args.list_devices
if list_devices:
my_devices = load_devices()
display_inventory(my_devices)
return 0

# CLI ARGS #####
cli_args = parse_arguments(args, COMMAND)
cli_vars = extract_cli_vars(cli_args, command=COMMAND, __version__=__version__)
device_or_group = cli_args.devices.strip()
hide_failed = cli_args.hide_failed

# CFG COMMAND HANDLER #####
cfg_command = cli_args.cmd
if cfg_command:
if r"\n" in cfg_command:
Expand All @@ -111,15 +38,11 @@ def main(args):
cfg_command = command_data.splitlines()
else:
raise ValueError("No configuration commands provided.")
device_or_group = cli_args.devices.strip()
pattern = r"."
hide_failed = cli_args.hide_failed

# DEVICE LOADING #####
devices = obtain_devices(device_or_group)

# Retrieve output from devices
my_files = []
failed_devices = []
results = {}

Expand All @@ -128,9 +51,9 @@ def main(args):
for device_name, device_params in devices.items():
update_device_params(
device_params,
username=cli_username,
password=cli_password,
secret=cli_secret,
username=cli_vars["cli_username"],
password=cli_vars["cli_password"],
secret=cli_vars["cli_secret"],
)
device_tasks.append(
{
Expand All @@ -147,30 +70,35 @@ def main(args):
device_name, output = future.result()
results[device_name] = output

netmiko_base_dir, netmiko_full_dir = find_netmiko_dir()
ensure_dir_exists(netmiko_base_dir)
ensure_dir_exists(netmiko_full_dir)
# FIND FAILED DEVICES #####
# NEED NEW WAY TO CACHE AND RE-USE CACHED FILES
valid_results = {}
for device_name, output in results.items():
file_name = write_tmp_file(device_name, output)
if ERROR_PATTERN not in output:
my_files.append(file_name)
else:
# Cache output(?)
# file_name = write_tmp_file(device_name, output)
if ERROR_PATTERN in output:
failed_devices.append(device_name)
continue
valid_results[device_name] = output

# OUTPUT PROCESSING #####
out_format = "text"
if cli_args.json and cli_args.raw:
out_format = "json_raw"
elif cli_args.json:
out_format = "json"
elif cli_args.raw:
out_format = "raw"
# elif output_yaml:
# out_format = "yaml"
output_dispatcher(out_format, valid_results)

grep_options = []
grepx(my_files, pattern, grep_options)
if cli_args.display_runtime:
print("Total time: {0}".format(datetime.now() - start_time))

if not hide_failed:
if failed_devices:
print("\n")
print("-" * 20)
print("Failed devices:")
failed_devices.sort()
for device_name in failed_devices:
print(" {}".format(device_name))
print()
output_failed_devices(failed_devices)

return 0


Expand Down
Loading

0 comments on commit 3e0623e

Please sign in to comment.