New to Trigger, unsure how to proceed

99 views
Skip to first unread message

John Neiberger

unread,
Feb 11, 2014, 12:12:06 AM2/11/14
to trigge...@googlegroups.com
tl;dr  I think I'm making this more complicated than it needs to be. I'd like to get some tips on using Trigger because I'm really unsure how to use it the way it was intended to be used. Read on for details. My apologies for my verbosity.

I've got Trigger installed and configured with some test devices in netdevices.xml. I've read the docs and tried a few things and I'm still unsure how to proceed. I'm still a beginner to python and want to write some scripts for our group at work. I began by writing some stuff using telnetlib but realized quickly that I was doing it the hard way and should have used pexpect instead. While searching for some examples of using pexpect, I ran across a recommendation for Trigger, which is how I got here. It seems like a really fantastic tool but I have to admit that I don't yet know how to make the best use of it.

My goal was to do expect-style scripting without the hassle. Trigger's asynchronous utilities seem to take care of that, but at a cost. I'm very new to Twisted, as well, and I'm just now getting caught up with how chains work. I'm not sure how to use them to do what I want to do. Here is a real life example (copied from my explanation in IRC):

We have run across a bug on Cisco 7600/6500s that occasionally stops inbound service policies from working. We use ingress policies for DSCP marking. If the marking breaks, all sorts of other stuff breaks since we use DSCP markings for security, as well. So, I need to build a script that can go out to all of the 7600s and 6500s my group manages and check for broken service policies. My idea is to run a query on NetDevices to get a list of actual devices. The script would interate through the list and check each one. On each device, I need to run a command, like some variation of "show policy-map" or maybe even just a "show run" and parse it to get a list of interface with ingress service policies. Then I need to iterate through that list and run "show mls qos ip <interface>" on each interface to verify that ingress packet marking counters are non-zero. If any are zero, that means we've hit the bug, so I need to log the device and interface for review later. 

I suppose I could use Commando to iterate through a list of devices and run all possible commands that I might need and then save the output to a file, then later separately pull those files in and parse them to get the data I need, but that wasn't my original idea. 

Here is a very simple example of what I came up with using async callbacks:

from trigger.netdevices import NetDevices
from twisted.internet import reactor

nd = NetDevices()
dev = nd.find('labrouter')

def print_version(data):
    print(data[0])

def print_siib(data):
    print(data[0])

def stop_reactor(data):
    print 'Stopping reactor'
    if reactor.running:
        reactor.stop()



async = dev.execute(['show version'])
async.addCallback(print_version)

async = dev.execute(['show ip interface brief'])
async.addCallback(print_siib)

async.addBoth(stop_reactor)
reactor.run()

That works, but I can only run reactor once, so I can only talk to one device. That's just not going to work for me. Incidentally, someone on /r/learnpython suggested I learn to use inline callbacks. I guess I see what they're doing, but it seriously increased the complexity of the code. Here is an example of that (most of this code came from the redditor helping me):

import sys
from trigger.netdevices import NetDevices
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python import log
import re

@inlineCallbacks
def getInterfaces(dev):
    interfaces = yield dev.execute(['show ip int brief'])
    # Insert code here to parse `stuff`.
    returnValue(interfaces)


@inlineCallbacks
def main():
#    log.startLogging(sys.stdout)
    nd = NetDevices()
    dev = nd.find('labrouter')

    version = yield dev.execute(['show version'])
    #log.msg('Got version: {}'.format(version))
    m = re.search(r'Version (.*),', version[0], re.M|re.I)
    if not m:
        raise ValueError('Version not found!')
    else:
        print 'Version is ', m.group(1)


    interfaces = yield getInterfaces(dev)
    for interface in interfaces:
        #log.msg('Doing stuff with {}'.format(interface))
        if not interface.startswith('Interface'):
            print interface
        # Assuming doStuffWithInterface returns a Deferred:
       # yield doStuffWithInterface(dev, interface)


if __name__ == '__main__':
    main().addBoth(lambda _: reactor.stop())
    reactor.run()

I decided to just try my hand at pexpect since I had never used it before. Here is a short example of that, which works but then I don't get the benefits of Trigger:

import pexpect
import sys

HOST = ['192.168.1.107']
USERNAME = 'lab'
PASS = 'cisco'
ENABLE = 'cisco'


def ios_initial_cmds():
    child.expect(['>', '#'])
    child.sendline('term len 0')

command_list = ['show version', 'show ip int brief', 'show ip route']

child = pexpect.spawn('telnet', HOST)
#child.logfile = sys.stdout
child.expect ('Username:')
child.sendline(USERNAME)
child.expect('Password:')
child.sendline(PASS)
ios_initial_cmds()
child.expect(['>', '#'])
p = child.before.split('\n')[1]

UNPRIV_PROMPT = p + ">"
ENABLE_PROMPT = p + "#"
PROMPTS = [UNPRIV_PROMPT, ENABLE_PROMPT]

for command in command_list:
    child.sendline(command)
    child.expect(PROMPTS)
    out = child.before
    print out

child.sendline('exit')


So, what approach would you recommend? I'm leaning toward the Commando approach with separate log parsing modules to get the data I ultimately really need instead of parsing the device output as I get it. What do you think?

Thanks!

Jathan McCollum

unread,
Feb 13, 2014, 12:08:02 PM2/13/14
to trigge...@googlegroups.com
Help John and welcome to Trigger-

First of all, if you've seen all the emails I've caught up on today it's obvious that you're not alone. I need to better document how to get started writing command templates using Trigger's Commando class.

I'm sorry you are stuck, so let me see if I can help.

The goal of the trigger.cmds.Commando class is to avoid having to worry about the underlying mechanics of the Twisted library that Trigger uses for async network I/O. So, at the most base level if you just want to execute a series of commands on a list of devices.

Using the data from the sample code you provided, it should be as simple as this:

from trigger.cmds import Commando

command_list = ['show version', 'show ip int brief', 'show ip route']
device_list = ['192.168.1.107']

runner = Commando(devices=device_list, commands=command_list)
runner.run()

# After it's done the .results attribute is a dict all of the results keyed
# by the device name/IP, each of which is a dict keyed by the command
# executed.
print runner.results['192.168.1.107']['show ip int brief']

To create a specialized command runner, say for "show clock", it's as easy as:

class ShowClock(Commando):
    """Execute 'show clock' on Cisco devices."""
    vendors = ['cisco']
    commands = ['show clock']

showclock = ShowClock(devices=device_list)

To create a specialized "show clock" command runner that parses the timestamps into datetime objects (illustrating the definition of a from_cisco() method to parse the output when it comes back):

class ShowClock(Commando):
    vendors = ['cisco']
    commands = ['show clock']

    def from_cisco(self, results, device):
        # => '16:18:21.763 GMT Thu Jun 28 2012\n'
        fmt = '%H:%M:%S.%f %Z %a %b %d %Y\n'
        self._store_datetime(results, device, fmt)

    def _store_datetime(self, results, device, fmt):
        parsed_dt = self._parse_datetime(results, fmt)
        self.store_results(device, parsed_dt)

    def _parse_datetime(self, datestr, fmt):
        try:
            return datetime.strptime(datestr, fmt)
      except ValueError:
          return detester

showclock = ShowClock(devices=device_list)

To parse interfaces, before you go too far, I'd like to recommend that you check out trigger.cmds.NetACLInfo, which is used by the "gnng" command that comes bundled with Trigger. This tool fetches interface information for a device and displays them in a table format.

The NetACLInfo subclass of Commando is a good example of how you can define to_<vendor> and from_<vendor> methods to heavily customize the behavior of how commands are sent to devices, and what to do with the results that come back.

Check out the NetACLInfo source code, and PLEASE let me know if you have any questions:



--
You received this message because you are subscribed to the Google Groups "Trigger" group.
To unsubscribe from this group and stop receiving emails from it, send an email to trigger-user...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.



--
Jathan.
--

John Neiberger

unread,
Feb 13, 2014, 12:23:22 PM2/13/14
to trigge...@googlegroups.com
Thanks! I'll take another look at it. I'll give you another example from something that came up the past few days related to a "feature" we ran into on our 7600s. I needed to write a script to pull in a list of devices, run "show tcp brief | i SYNRCVD" and then pull out just one column of info from the resulting command. Then, to mitigate the issue, I need to iterate through that list and issue the command "clear tcp tcb <value>". If I were to use Commando, I take it that I'd have to write a class for this and make the parsing function a method in the class, right? I'm not quite sure how it all would look, but I should again qualify that I'm a noob when it comes to Python. I'm learning rapidly, but be patient. lol  As a reference, here is a sanitized version of the script I wrote using pexpect. This does not include the mitigation piece. I would LOVE to not have to deal with handling the replies from pexpect to get just the info I need. I love that Trigger abstracts that stuff away from me. On the other hand, my programming skills are improving from learning how to do it.  hehe  As you can see, I need to learn to be more OOP with my code. 

import pexpect
import sys

HOST = ['10.1.1.1']
USERNAME = 'lab'
PASS = 'cisco'
ENABLE = 'cisco'

def router_audit(HOST):
    # Connect to host and login using username and password
    child = pexpect.spawn('telnet', HOST)
    #child.logfile = sys.stdout
    child.expect ('Username:')
    child.sendline(USERNAME)
    child.expect('Password:')
    child.sendline(PASS)
    child.expect(['>', '#'])
    # Send 'term len 0' to avoid needing to deal with the MORE prompt
    child.sendline('term len 0')
    child.expect(['>', '#'])

    # This grabs the hostname, from which
    # we can construct a privileged mode prompt and a
    # non-privileged mode prompt to be used in later 'expect' lines
    p = child.before.split('\n')[1]

    UNPRIV_PROMPT = p + ">"
    ENABLE_PROMPT = p + "#"
    PROMPTS = [UNPRIV_PROMPT, ENABLE_PROMPT]

    # Check for privileged mode and login to enable mode, if necessary
    if child.after == ">":
        child.sendline('enable')
        child.sendline(ENABLE)
        child.expect(PROMPTS)

    # Check for TCP sockets stuck in SYNRCVD state
    child.sendline('show tcp brief | i SYNRCVD')
    child.expect(PROMPTS)

    # Grab the output from the previous command and parse through it to
    # get a new list of only the TCB values for sockets that are stuck
    out_list = child.before.split('\n')
    out_list = out_list[1:]  #  remove first line from output since that is the command itself
    tcb_list = [i.split()[0] for i in out_list if i != '']

    # If the tcb_list is not empty, print the hostname and the list of
    # stuck TCP sockets. The HOST variable is a list, so we're referencing
    # the first element to prettify the output, e.g. router.company.net
    # instead of ['router.company.net']
    if tcb_list:
        print '\nRouter', HOST[0], ' has the following stuck TCBs:\n'
        for i in tcb_list: print i
    else:
        print HOST[0], 'has no stuck TCP sockets.'
    child.sendline('exit')

router_audit(['10.255.151.2'])

'''
# The following code could be added to take in a list of routers and
# iterate through the list. Since pexpect.spawn() requires the
# passed value to be a list and not a string, we have to create
# an empty list named HOST and then add the device name to it.
# This way, HOST is the list ['router.company.net'] instead of the
# string 'router.company.net'.

routerList = []
with open('router_list_file.txt', 'r') as f:
    routerList = f.readlines()

for device in routerList:
    HOST = []
    HOST.append(device)
    router_audit(HOST)
'''



Jathan McCollum

unread,
Feb 20, 2014, 10:52:25 AM2/20/14
to trigge...@googlegroups.com
The logic you put in there to parse the output coming back from the device would fit perfectly in a from_cisco() method. Something like this:

class SynRcvdChecker(Commando):
    commands = ['show tcp brief | i SYNRCVD']

    def from_cisco(self, results, device):
        result = results[0] # There should only be one result
        out_list = result.split(\n')# Split the result on newlines
        tcb_list = [i.split()[0] for i in out_list if i != '']

        # If the tcb_list is not empty, print the hostname and the list of
        # stuck TCP sockets. The HOST variable is a list, so we're referencing
        # the first element to prettify the output, e.g. router.company.net
        # instead of ['router.company.net']
        if tcb_list:
            print '\nRouter', HOST[0], ' has the following stuck TCBs:\n'
            for i in tcb_list: print i
        else:
            print HOST[0], 'has no stuck TCP sockets.'

router_audit = SynRcvdChecker(devices=['10.255.151.2'])
router_audit.run()

John Neiberger

unread,
Feb 20, 2014, 11:04:25 AM2/20/14
to trigge...@googlegroups.com
Thanks! I'll check into doing it that way. We may need to get even more creative because we're running into twisted reactor problems when calling these test scripts from Django. They run once and then we get the "reactor not restartable" error. We're looking into solutions for that problem.

Regarding this, is the from_cisco() method called automatically? I haven't looked at the code under the hood. If we call router_audit.run(), what calls from_cisco()?

thanks again!
John

Jathan McCollum

unread,
Feb 20, 2014, 11:15:11 AM2/20/14
to trigge...@googlegroups.com
The to_foo() methods are called when commands are sent to a device, where "foo" is the canonical vendor name (cisco, juniper, etc.) based on how you mapped them in the device metadata. By default, from_foo() is called whenever results come back from the device.

This behavior is customizable. By default to_ will always just send the commands one after the other, and from_ will store the results in a dictionary keyed by the device hostname and each command, e.g.:

print router_audit.results
{'device1': {'show run': 'config would be here'} }
Reply all
Reply to author
Forward
0 new messages