Network Automation using YANG Models across XE, XR, & NX

Bonus Content - TextFSM CLI Parsing


We aren't always afforded the privilege of having YANG models for every possible device or traditional CLI based data gathering exercise. In this module we'll show you a practical way of scaling CLI parsing for scenarios where MDP (Model Driven Programmability) is not an available option.

What is TextFSM?

TextFSM (Text Finite State Machine) is a Python module that helps with parsing CLI and text based output in a reliable and consistant fashion. Many times as part of network test and orchestration automation, one encounters situationas where there is absolutely no choice other that parsing CLI output. This is where TextFSM can shine. In this bonus module, we use Paramiko and TextFSM to get structured Python data from unstructured CLI data using a deterministic finite state machine. Additional Links:

  1. TextFSM Source
  2. TextFSM Wiki
  3. Programmatic Access to CLI Devices with TextFSM

Install TextFSM

Install TextFSM into your container environment:

    
        pip3.6 install textfsm
    
    
    [root@cfa2c7b31323 workspace]# pip3.6 install textfsm
    Collecting textfsm
      Downloading https://files.pythonhosted.org/packages/a1/0d/a1b490503545b3b4600b965eae5d44cc2b6ce27cfb44f4debc563dbb56d3/textfsm-0.4.1.tar.gz
    Installing collected packages: textfsm
      Running setup.py install for textfsm ... done
    Successfully installed textfsm-0.4.1
    [root@cfa2c7b31323 workspace]#
    

Install Paramiko

We're going to use Paramiko to interact with the CLI over SSH. Go ahead and pip install it.

    
    pip3.6 install paramiko
    

Create Script

Create a new script called textfsm_cli_parse.py in your workspace:

    
        textfsm_cli_parse.py
    

Imports

With pip installs done, you now need the following imports. Also go ahead and define the access information to your XRv device. Conceptually the CLI parsing concepts you learn in this module can be applied to any device that sends text-based CLI output.

    
    import textfsm
    import paramiko
    import tempfile
    import time

    # Connection info
    ip = '10.2.100.12' # XRv
    user = 'admin'
    pwd = 'cisco.123'
    

Declare the Template

In this exercise, we want to take unstructured CLI output from the "show clock detail" command, and have that converted into a Python datastructure more useful for programming and network automation. TextFSM operates as a state machine, taking raw CLI output and a template as the inputs to the state machine, and producing structured data in the output of the state machine. Below is one example of using a TextFSM template for parsing "show clock detail" on XRv.

    
template_show_clock_detail = r"""Value month (\w+)
Value year (\d+)
Value day (\d+)
Value hour (\d+)
Value minute (\d+)
Value sec (\d+)
Value millisec (\d+)
Value timesrc ([a-zA-Z ]+)
Value timezone (\w+)
Value wday (\w+)

Start
  ^${hour}:${minute}:${sec}\.${millisec}\s\w+\s${wday}\s${month}\s${day}\s${year}
  ^Timezone:\s${timezone}\sTimesource:\s${timesrc}
"""
    

The Parse Function

We need to define a function that takes a TextFSM template, and raw CLI output, and returns structured data using TextFSM. The comments in the function describe what's happening in more detail.

    
def parse_textfsm(template, output):
# Define the text processing template for show clock detail output.

    # Create temp file to hold template.
    tmp = tempfile.NamedTemporaryFile(delete=False)

    # Write template to file for textfsm.
    with open(tmp.name, 'w') as f:
        f.write(template)

    # Get read handle for textfsm.
    with open(tmp.name, 'r') as f:
        # Instantiate a new TextFSM wrapper.
        fsm = textfsm.TextFSM(f)
        # Parse the output text according to template rules.
        fsm_results = fsm.ParseText(output)
        # Convert to list of dictionaries since TextFSM may return multiple
        # 'row' results.
        parsed = [dict(zip(fsm.header, row)) for row in fsm_results]

    # Return the parsed data.
    return parsed
    

Define the Command Executor

We also need to define a function to execute commands on a device using Paramiko. The function below takes a given command, and executes it over SSH using Paramiko on the device you supplied in your globals at the beginning.

    
# Define a function that we'll use to execute and return CLI output.
def cmd(command):
    """
    Runs a CLI command on a given SSH session, returning output.
    """
    # Create new paramiko instance
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    ssh.connect(ip, username=user, password=pwd)
    # Run command and get stdin, stdout, std err streams.
    stdin, stdout, stderr = ssh.exec_command(command)
    # Close this handle, not using it.
    stdin.close()
    # Read regular stdout and std err
    output_ok = stdout.read()
    output_err = stderr.read()
    # Return error text if exist else regular std out.
    # Convert to utf-8 string since python3 paramiko gives byte object
    if output_err:
        return output_err.decode('utf-8')
    else:
        return output_ok.decode('utf-8')
    

Putting it All Together

With all the prerequisite functions defined, you can now execute the "show clock detail" command on the XRv device and observe the structured output. In this example we call "show clock detail" twice, parsing and printing the structured list-of-dictionaries output to demoonstrate that each parsed data structure contains the key-value pairs corresponding to the template we provided the state machine.

    
# Get show version output.
output_t0 = cmd(command="show clock detail")

# Parse the show clock details.
parsed_t0 = parse_textfsm(template=template_show_clock_detail, output=output_t0)

# Wait for a second to let clock advance.
time.sleep(5)

# Get show version output.
output_t1 = cmd(command="show clock detail")

# Parse the show clock details.
parsed_t1 = parse_textfsm(template=template_show_clock_detail, output=output_t1)

# Print the clock details to console.
# Print out the clock details
print(parsed_t0)
print(parsed_t1)
    

The Full Script

At this point your script should look as follows:

    
    import textfsm
    import paramiko
    import tempfile
    import time

    # Connection info
    ip = '10.2.100.12' # XRv
    user = 'admin'
    pwd = 'cisco.123'

    template_show_clock_detail = r"""Value month (\w+)
    Value year (\d+)
    Value day (\d+)
    Value hour (\d+)
    Value minute (\d+)
    Value sec (\d+)
    Value millisec (\d+)
    Value timesrc ([a-zA-Z ]+)
    Value timezone (\w+)
    Value wday (\w+)

    Start
    ^${hour}:${minute}:${sec}\.${millisec}\s\w+\s${wday}\s${month}\s${day}\s${year}
    ^Timezone:\s${timezone}\sTimesource:\s${timesrc}
    """

    def parse_textfsm(template, output):
    # Define the text processing template for show clock detail output.

        # Create temp file to hold template.
        tmp = tempfile.NamedTemporaryFile(delete=False)

        # Write template to file for textfsm.
        with open(tmp.name, 'w') as f:
            f.write(template)

        # Get read handle for textfsm.
        with open(tmp.name, 'r') as f:
            # Instantiate a new TextFSM wrapper.
            fsm = textfsm.TextFSM(f)
            # Parse the output text according to template rules.
            fsm_results = fsm.ParseText(output)
            # Convert to list of dictionaries since TextFSM may return multiple
            # 'row' results.
            parsed = [dict(zip(fsm.header, row)) for row in fsm_results]

        # Return the parsed data.
        return parsed

    # Define a function that we'll use to execute and return CLI output.
    def cmd(command):
        """
        Runs a CLI command on a given SSH session, returning output.
        """
        # Create new paramiko instance
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(ip, username=user, password=pwd)
        # Run command and get stdin, stdout, std err streams.
        stdin, stdout, stderr = ssh.exec_command(command)
        # Close this handle, not using it.
        stdin.close()
        # Read regular stdout and std err
        output_ok = stdout.read()
        output_err = stderr.read()
        # Return error text if exist else regular std out.
        # Convert to utf-8 string since python3 paramiko gives byte object
        if output_err:
            return output_err.decode('utf-8')
        else:
            return output_ok.decode('utf-8')

    # Get show version output.
    output_t0 = cmd(command="show clock detail")

    # Parse the show clock details.
    parsed_t0 = parse_textfsm(template=template_show_clock_detail, output=output_t0)

    # Wait for a second to let clock advance.
    time.sleep(5)

    # Get show version output.
    output_t1 = cmd(command="show clock detail")

    # Parse the show clock details.
    parsed_t1 = parse_textfsm(template=template_show_clock_detail, output=output_t1)

    # Print the clock details to console.
    # Print out the clock details
    print(parsed_t0)
    print(parsed_t1)
    

Execute Python Script

    
        python3.6 textfsm_cli_parse.py
    

The prints should yield something along the lines of:

        
            [{'month': 'May', 'year': '2018', 'day': '25', 'hour': '15', 'minute': '32', 'sec': '54', 'millisec': '083', 'timesrc': 'Hardware calendar', 'timezone': 'UTC', 'wday': 'Fri'}]
            [{'month': 'May', 'year': '2018', 'day': '25', 'hour': '15', 'minute': '32', 'sec': '59', 'millisec': '964', 'timesrc': 'Hardware calendar', 'timezone' 'UTC', 'wday': 'Fri'}]
        
    
Each print statement's output corresponds to a call to "show process details" structured parsing of the raw XRv CLI output for that command.
        
            mt-csrv#show clock detail
            *16:33:56.880 UTC Fri May 25 2018
            No time source
            mt-csrv#
        
    

Summary

This was just a simple example showing how one can take a few foundational Python constructs, namely, a function for getting the CLI output, a function for taking CLI Output + Template = Structured output, and a template, and then creating very succinct Python call patterns for repeatable & deterministic CLI parsing. Many show commands already have open source templates available, for example, the Network to Code templates on GitHub. If you find yourself stuck parsing CLI, remember that using consistant templates are more efficient than having regular expresions sprinkled all around a large library of scripts; rather; doing the raw-to-structured translation through centralized tempaltes is good from a re-useability perspective.