Building a Production-ish Ready WebEx Teams ChatBot

Introduction

  • Is your interrupt-driven day no longer supportable?
  • Is there a particular Project Manager that asks you the same question every morning?
  • Do you often have to take some technical data and simplify it for semi- or non-technical consumption?
  • Would you like to pull out relevant sections of technical data for a sibling team? Today you send them all the data and hope they can pull out what they need, or find what they need because you just don’t have the time to put together something that is customized for them containing only what they need?

I suspect many of us have these situations far too often.

I spend quite a bit of time on Webex Teams (referred to as Spark from here on out in protest of the terrible re-branding of Spark to WebEx Teams which not only completely lacks imagination but also confuses anyone who also has to deal with Microsoft Teams…. a rebranding so awful it might actually unseat the rebranding of brigade). It is currently the messaging application of choice for my two biggest projects and a principal culprit in my interrupt-driven day. Maybe this can help turn it into an advantage… a mini-you if you will.

Not long ago, I lost a young and incredibly capable engineer to another project and that loss was pretty impactful. Many of the day to day activities fell back to me and I found lots of little (and not so little) things falling through the cracks. How was I going to work smarter not harder?

Network engineer getting asked questions from everyone!

The Back Story

I got to attend Interop this year and was very inspired by every session I attended but a few in particular really got me thinking about how I might automate my way out of my current predicament.

Jeremy Schulman– – Self-service Network Automation Using Slack

Nick RussoAutomation for Bureaucracies

Hank PrestonA Practical Look at NetDevOps – While Hank’s session inspired other ideas it is his work with DevNet and a SparkBot module that is most relevant for this exercise.

The Resulting Scene

The effort documented here is a real world experience and solution. To protect the innocent I’ve created a “demo” version of this solution that will highlight the functionality and the possibilities. Over the course of several posts, I’ll document the requirements, use cases, solutions, and issues.


I like to start with the problem statement:

  1. I need to be able to provide fine grained project status on demand
  2. I need to be able to print a pretty Summary report for management on demand
  3. I need to be able to extract the relevant portion of some technical information for a parallel project on demand
  4. I’d like to incorporate something fun to get the team comfortable and familiar with the Bot (on demand of course)

I also like to document the initial state and the characteristics of the environment:

  • Spark was the required platform for messaging. Enough of the team utilized Spark so as to make it an extremely viable delivery platform.
  • A SAAS platform was used for project status.
  • All the collateral and technical information resided in a document repository that everyone constantly works on and syncs to.

The solution concept: Develop a SparkBot attendant available to the Spark Team under which all of our spaces are created which can access an external API, a document repository, and respond to simple commands starting with:

  1. site_status
  2. site_summary
  3. site_networks
  4. comic_relief

OK..I started Googling and found lots of material but there were two issues:

  • Almost everything leveraged a dummy test environment often using ngrok, a fabulous little tool, but I needed this to be at least in the neighborhood of production ready. I did not want it to run on my home system.
  • Assembly was required in that there was good material on specific steps but not much on putting it all together. I suspect the thought there was that some of that should have been obvious but it wasn’t to me.

When I knew enough to be dangerous I figured I needed the following components:

ComponentDescriptionPlatform Used
Messaging Client & Account
The most basic requirement was a messaging client that was in use by enough of the team so as to be a viable delivery platform. This one was easy. Spark was already the messaging application of choice for the project. Using the messaging client solved all kinds of issues. I already had a client for a wide range of platforms, web, desktop, and mobile and I had a known User Interface to which the team was already accustomed.


Cisco WebEx Teams (Spark!!) Web Client, Desktop App, Mobile App
Messaging PlatformOf course the back end messaging platform had to support the required functionality for a Bot type application and research showed that it did. Spark has all the hooks for provisioning a Webhook and a Bot application.

Cisco WebEx Teams (Spark!!)
Document RepositoryThe Document repository the Team used had to have a client that I could install on the Web Server so that the data store would be available to bot functions and would always be synchronized (the latest).

Google Drive
Project Status APIThe Project Status tool had to support an API and I knew that the project SAAS did. (More on this later)

SAAS Project Management Tool
Web Server (Front End)Here is where it started getting tricky. I am by no means a System Administrator but I had to find a suitable web server and web server technology to use. The Web Server had to support the back end environment for the functionality and as I mentioned, also had to have a client so that I could present and sync the document repository.

Nginx
Web Server BackendFor the back end technology I always gravitate to Django but it was not a good fit in this case.
1. I did not foresee a need for a database
2. Most of the examples I found utilized Flask and I needed to get this working quickly before the village came after me with pitchforks and torches.

This was my first use of Flask and it is a testament to what you always here about it. It is simple and lightweight with quite a lot of functionality. More than enough for my purposes. With the excellent examples out there I had no issues getting it to work.

Flask & Python
Hosting PlatformThis was another challenge.

I mentioned I’m not a SysAdmin and initially I felt that bringing up a Linux server that I could “harden” sufficiently was probably not something I was going to do well so I started with a MacInCloud instance. It was a good Proof of Concept but I wasn’t happy with the performance or the cost so I abandoned it as the platform for the Bot Server.

There was nothing for it.

I needed to go down the path of a Linux host. I think I knew that already but I’ve been wanting to check out MacInCloud for a while and I thought that route would also save me time in researching how to secure a Linux server.
I always gravitate to Digital Ocean for this. They have superb documentation and How-Tos, simple and intuitive interface, excellent price points, and I’m a diver.

The first iteration of the Bot was actually on Digital Ocean and working quite well but I ran into a showstopper. The document sync client for Linux did not give you the option of changing the mount point. I could not grow the home volume on my Droplet sufficiently (the project data store is at about ~500GB) to host the data. I could add a volume but the sync client would try to sync to the home directory. I found a hack but it talked about sync instability and if I could not rely on the data being in sync on the Web Server then the rest was going to be pointless. Much of the functionality I needed involved accessing the latest data and documents on the repository.

Well, the other hosting platform I’ve been wanting to try is the Google Compute Platform. I must say I’m very impressed. Everything is pretty intuitive. I already had the domain I wanted to use on Google so that made DNS a breeze!

The production Bot is currently on GCP.

The public Demo system that is part of this post is on Digital Ocean. The Demo does not have the constraints of the production system and without the Digital Ocean documentation its doubtful I would have a working solution.

MacInCloud
Digital Ocean Droplet
Google Compute Platform (GCP)
Note: The Demo is built on a Digital Ocean Droplet
Messaging Platform SDK or ModuleI knew Spark had an SDK so worst case I had tools for the Bot.

Further research resulted in quite a few options for the Bot.

Here is where Hank Preston really saved me a ton of time. Hank’s webexteamsbot module provides a very nice framework for your own bot functions. It comes with a few that you will want to keep (/help is one) and provides some very good examples.

As your bot gets more capable (i.e. understands new functions or commands) you simply add those functions to your main script. The biggest issue I had was understanding what the module abstracted out. I needed to tap into a lot of the information…for example, the Spark space or room name had information that I needed to parse out so that if you ran a command within a space it would customize the output.

hpreston/webexteamsbot
ScriptsEach “function” or Bot command would need its own script or set of scripts and functions.

Python
HTTPS
I wanted to include as much security as my feeble SysAdmin abilities could muster. At a minimum SSL and some basic firewall functions.

HTTPS
certbot
DomainI wanted to use a FQDN.

cdl-automation.net
The Webex Teams ChatBot Subsystems
The ChatBot Subsystems

This is the first of a 5 Part Series on creating a production-ish ready WebEx Teams Chat Bot.

  1. Introduction
  2. The Basic Web Server
  3. The WebEx Teams (Spark) Backend Webhook
  4. The Bot Application on the Web Server
  5. The Bot

Building a Custom TextFSM Template

If you have seen any of the TextFSM posts on this site you know how useful the Network To Code TextFSM Template repository can be. Rarely do I not find what I need there!

I recently had to parse route summary information from JUNOS Looking Glass routers. I always check the very rich set of templates in the NTC Template index repository but in this case I was out of luck. I was going to have to build my own… and you get to watch.

Two fantastic resources you can use when you are in the same boat are here:

Its good to begin by familiarizing yourself with the output you need to parse. Here is a snippet of the show command output.

>show route summary
Autonomous system number: 2495
Router ID: 164.113.193.221
inet.0: 762484 destinations, 1079411 routes (762477 active, 0 holddown, 12 hidden)
Direct: 1 routes, 1 active
Local: 1 routes, 1 active
BGP: 1079404 routes, 762470 active
Static: 5 routes, 5 active
inet.2: 3073 destinations, 3073 routes (3073 active, 0 holddown, 0 hidden)
BGP: 3073 routes, 3073 active

Start with something simple like ASN and RouterID

A basic TextFSM Template

I wanted to start slowly with something I knew I could get to work. Looking at the data, it should be simple to extract the first two values I need:
– ASN
– Router ID

I started with those values as they are by far the simpler to extract from the ‘show route summary’ command. I will try not to cover material that is covered by the two Google links above. However I do want to point out the concept of TextFSM (as I understand it or explain it to myself) which is to provide context for your regular expressions. That is, not only can you define the specific pattern to search for but you can also define its “environment”. As you can see below the “Value” keyword lets me define a variable I want to pluck out of the unstructured text (the show command output). LIne 4 defines the “action” section to start processing and the first thing to look for is a line that starts with “Autonomous system number:” one or more space noted by the \s+ and then our ASN variable which we defined above as being a pattern of one or more digits \d+. So you have the power of the regular expression that defines the value you want and the power of regular expressions to help you define the context where your value will be found.

Junos ‘show route summary’ TextFSM Template – Version 1

For this exercise we will use my textfsm3 GitHub repository and the “test_textfsm.py” script for our testing rather than the Python command interpreter. Simply clone the repository to get started.
Note that the repository has the completed version of the template. Look at the history of the template file on GitHub to see its “evolution”.

(txtfsm3) Claudias-iMac:textfsm3 claudia$ python test_textfsm.py -h
usage: test_textfsm.py [-h] [-v] template_file output_file
This script applys a textfsm template to a text file of unstructured data (often show commands). The resulting structured data is saved as text (output.txt) and CSV (output.csv).
positional arguments:
template_file TextFSM Template File
output_file Device data (show command) output
optional arguments:
-h, --help show this help message and exit
-v, --verbose Enable all of the extra print statements used to investigate the results

In the first iteration of the template file, we obtain the output below.

(txtfsm3) Claudias-iMac:textfsm3 claudia$ python test_textfsm.py junos_show_route_summary
.template junos_show_route_summary.txt

TextFSM Results Header:
['ASN', 'RTRID']
================================
['2495', '164.113.193.221']
================================

Extract more details

So we have successfully built a template that will extract ASN and RouterID from the Junos show route summary command. Now it will get interesting because we also want this next set of values.

  • Interface
  • Destinations
  • Routes
  • Active
  • Holddown
  • Hidden

The first challenge here was to pick up the totals line. Here, one of my favorite tools comes into play, RegEx101. Regular expressions don’t come easy to me and this site makes it so easy! I saved the working session for trying to match the first part of that long totals line. As you can see, you can’t just match “inet”, or “inet” plus a digit, you also have to account for the “small.” Using RegEx101 and trial and error I came up with the following regular expression.

Value INT (([a-z]+.)?[a-z]+(\d)?.\d+)

inet.0: 762484 destinations, 1079411 routes (762477 active, 0 holddown, 12 hidden)

inet6.0: 66912 destinations, 103194 routes (66897 active, 0 holddown, 30 hidden)
Direct: 3 routes, 3 active

small.inet6.0: 31162 destinations, 31162 routes (31162 active, 0 holddown, 0 hidden)
BGP: 31162 routes, 31162 active

Let’s break it down…

The diagram below breaks the regex down into the key sections and numbers them. At the bottom you can see the actual text we are trying to parse and the numbers above indicate which section of the regex picked up the text we were interested in.

Breaking down the regular expression to extract the interface identifier (inet.x) for your TextFSM Template

The regex for INT (inet.x) was by far the most complicated. See 3 and 4 above. The rest of the line is far simpler and you just need to make sure you have it exactly as it appears in the raw text. Note that the parenthesis, which are part of the raw text show command, must also be ‘escaped’ just like the period.

Here is the TextFSM Template so far:

 Value Filldown ASN (\d+)
Value Filldown RTRID (\S+)
Value INT (([a-z]+.)?[a-z]+(\d)?.\d+)
Value DEST (\d+)
Value Required ROUTES (\d+)
Value ACTIVE (\d+)
Value HOLDDOWN (\d+)
Value HIDDEN (\d+)
Start
^Autonomous system number:\s+${ASN}
^Router ID:\s+${RTRID}
^${INT}:\s+${DEST}\s+destinations,\s+${ROUTES}\s+routes\s+\(${ACTIVE}\s+active,\s+${HOLDDOWN}\s+holddown,\s+${HIDDEN}\s+hidden\) -> Record

…and the resulting structured data:

(txtfsm3) Claudias-iMac:textfsm3 claudia$ python test_textfsm.py junos_show_route_summary.template junos_show_route_summary.txt
TextFSM Results Header:
['ASN', 'RTRID', 'INT', 'DEST', 'ROUTES', 'ACTIVE', 'HOLDDOWN', 'HIDDEN']
['2495', '164.113.193.221', 'inet.0', '762484', '1079411', '762477', '0', '12']
['2495', '164.113.193.221', 'inet.2', '3073', '3073', '3073', '0', '0']
['2495', '164.113.193.221', 'small.inet.0', '116371', '116377', '116371', '0', '0']
['2495', '164.113.193.221', 'inet6.0', '66912', '103194', '66897', '0', '30']
['2495', '164.113.193.221', 'small.inet6.0', '31162', '31162', '31162', '0', '0']

A few things to highlight, I used the ‘Filldown’ keyword for ASN and RTRID so that each “record” would have that information. The ‘Filldown’ keyword will take a value that appears once and duplicate it in subsequent records. If nothing else, it IDs the router from which the entry came but it also serves to simplify some things you might want to do down the line as each “record” has all the data. I also used the ‘Required’ keyword for routes to get rid of the empty last row that is generated when you used ‘Filldown’.

Almost there! We just need to pick up the source routes under each totals line.

Value SOURCE (\w+)
Value SRC_ROUTES (\d+)
Value SRC_ACTIVE (\d+)

Here is what the final (for now anyway) template looks like:

 Value Filldown ASN (\d+)
Value Filldown RTRID (\S+)
Value Filldown INT (([a-z]+.)?[a-z]+(\d)?.\d+)
Value DEST (\d+)
Value ROUTES (\d+)
Value ACTIVE (\d+)
Value HOLDDOWN (\d+)
Value HIDDEN (\d+)
Value SOURCE (\w+)
Value SRC_ROUTES (\d+)
Value SRC_ACTIVE (\d+)

Start
^Autonomous system number:\s+${ASN}
^Router ID:\s+${RTRID}
^${INT}:\s+${DEST}\s+destinations,\s+${ROUTES}\s+routes\s+(${ACTIVE}\s+active,\s+${HOLDDOWN}\s+holddown,\s+${HIDDEN}\s+hidden) -> Record
^\s+${SOURCE}:\s+${SRC_ROUTES}\s+routes,\s+${SRC_ACTIVE}\s+active -> Record

A few highlights. Because I wanted to store the source routes in a different value (SRC_ROUTES) I had to remove required from Routes in order to pick up the rows. I now have an extra row at the end but I can live with that for now. I also added Filldown to INT so that its clear where the source information came from.

(txtfsm3) Claudias-iMac:textfsm3 claudia$ python test_textfsm.py junos_show_route_summary.template junos_show_route_summary.txt

TextFSM Results Header:
['ASN', 'RTRID', 'INT', 'DEST', 'ROUTES', 'ACTIVE', 'HOLDDOWN', 'HIDDEN', 'SOURCE', 'SRC_ROUTES', 'SRC_ACT
IVE']
['2495', '164.113.193.221', 'inet.0', '762484', '1079411', '762477', '0', '12', '', '', '']
['2495', '164.113.193.221', 'inet.0', '', '', '', '', '', 'Direct', '1', '1']
['2495', '164.113.193.221', 'inet.0', '', '', '', '', '', 'Local', '1', '1']
['2495', '164.113.193.221', 'inet.0', '', '', '', '', '', 'BGP', '1079404', '762470']
['2495', '164.113.193.221', 'inet.0', '', '', '', '', '', 'Static', '5', '5']
['2495', '164.113.193.221', 'inet.2', '3073', '3073', '3073', '0', '0', '', '', '']
['2495', '164.113.193.221', 'inet.2', '', '', '', '', '', 'BGP', '3073', '3073']
['2495', '164.113.193.221', 'small.inet.0', '116371', '116377', '116371', '0', '0', '', '', '']
['2495', '164.113.193.221', 'small.inet.0', '', '', '', '', '', 'BGP', '116377', '116371']
['2495', '164.113.193.221', 'inet6.0', '66912', '103194', '66897', '0', '30', '', '', '']
['2495', '164.113.193.221', 'inet6.0', '', '', '', '', '', 'Direct', '3', '3']
['2495', '164.113.193.221', 'inet6.0', '', '', '', '', '', 'Local', '2', '2']
['2495', '164.113.193.221', 'inet6.0', '', '', '', '', '', 'BGP', '103185', '66888']
['2495', '164.113.193.221', 'inet6.0', '', '', '', '', '', 'Static', '4', '4']
['2495', '164.113.193.221', 'small.inet6.0', '31162', '31162', '31162', '0', '0', '', '', '']
['2495', '164.113.193.221', 'small.inet6.0', '', '', '', '', '', 'BGP', '31162', '31162']
['2495', '164.113.193.221', 'small.inet6.0', '', '', '', '', '', '', '', '']

The test_textfsm.py file will save your output into a text file as well as into a CSV file.
I did try using ROUTES for both sections and making it Required again. This got rid of the extra empty row but really impacts readability. I would have to keep track of how I used ROUTES as I would have lost the SRC_ROUTES distinction. That is a far greater sin in my opinion than an empty row at the end which is clearly just an empty row.

A quick example of using TextFSM to parse data from Cisco show commands

TextFSM is a powerful parsing tool (python module) developed by Google.    There are some great examples out there to get you started. Here are two I urge you to read if this topic is of interest to you:

I can never get enough of examples so here is a very simple one to get you started or keep you practicing.  I find that a quick example where I can see results of my own making really energizes my learning process.

For this example, you need python 2.7 and the textfsm module installed.

TextFSM Getting Started is an excellent resource which includes the installation process for textfsm (textfsm is a pip installable module).

In addition to the environment, you will need 2 things to get started.

  • A TextFMS template
  • Content to parse with the TextFMS template

The template may be the tricky part.  This template defines the structure of the data you are trying to gather or parse out of your content.  We are very fortunate that the Network to Code (NTC) team has given us a large library of templates from which to choose and so we don’t have to worry too much about the template itself. We just need to know what Cisco IOS show command has the information we want.

Once you have your template, you need content to parse.  You can get this in a variety of ways.  You can query your devices real time via Ansible or via a python scirpt or you can act on file based data that you already have.

In this example we will keep it simple and assume we have a text file of show commands that we need to parse to get the device hardware information and software version.

To get hardware and software information, the “show version” output should have what we want.  So looking at the existing templates in the NTC library, it looks like the cisco_ios_show_version.template has what we need, If we look at the template we can see that it has two variables, VERSION and HARDWARE (which will return a list).  That looks just about right for the information we want to extract and luckily the file of show commands includes the output of the “show version” command.

So here are the two files we will work with in this example:

– the textFSM template file (downloadable from GitHub)
ntc-templates/templates/cisco_ios_show_version.template

– the content file with show commands including the output of the show version command (downloadable here)
lab-swtich-show-cmds

For simplicity I’ve put them both in a temp directory and I will launch the python interpreter from there so we can work real time. I also list the modules that I have in the virtual environment. The only one you need for this example is textfsm.

Working directory and files
(textfsm) Eugenias-PB:textfsm-example eugenia$ pwd
/Users/eugenia/Downloads/textfsm-example
(textfsm) Eugenias-PB:textfsm-example eugenia$ ls
cisco_ios_show_version.template lab-swtich-show-cmds.txt
(textfsm) Eugenias-PB:textfsm-example eugenia$ ls -al
total 24
drwxr-xr-x 4 eugenia staff 136 May 11 05:06 .
drwx------+ 118 eugenia staff 4012 May 11 05:04 ..
-rw-r--r--@ 1 eugenia staff 680 May 11 05:06 cisco_ios_show_version.template
-rw-r--r--@ 1 eugenia staff 7425 May 11 05:05 lab-swtich-show-cmds.txt

The environment
(textfsm) Eugenias-PB:~ eugenia$ python --version
Python 2.7.15


(textfsm) Eugenias-PB:textfsm-example eugenia$ pip freeze
et-xmlfile==1.0.1
graphviz==0.8.2
jdcal==1.3
netaddr==0.7.19
openpyxl==2.5.1
**textfsm==0.3.2**
xlrd==1.1.0
(textfsm) Eugenias-PB:textfsm-example eugenia$
Lets get started…
Launch the interpreter and import the textfsm module.  The ">>>" tells you that you are in the python command interpreter. 


(textfsm) Eugenias-PB:textfsm-example eugenia$ python
Python 2.7.10 (default, Feb 7 2017, 00:08:15)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> import textfsm
Open the template file into a file handle I've called template and pass that as the argument to the textfsm.TextFSM method
creating a "parsing object" based on that template (the show version template in our case).


>>> template = open('cisco_ios_show_version.template')
>>> results_template = textfsm.TextFSM(template)
Look at some of the methods available in the results_template object by using dir() on the results_template object you just created.  Make note of the 'ParseText' one 
as that is the one we will use shortly to parse our content.


>>> dir(results_template)
['GetValuesByAttrib', 'MAX_NAME_LEN', 'ParseText', 'Reset', '_AppendRecord', '_AssignVar', '_CheckLine', '_CheckRule', '_ClearAllRecord', '_ClearRecord', '_DEFAULT_OPTIONS', '_GetHeader', '_GetValue', '_Operations', '_Parse', '_ParseFSMState', '_ParseFSMVariables', '_ValidateFSM', '_ValidateOptions', '__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_cur_state', '_cur_state_name', '_line_num', '_options_cls', '_result', 'comment_regex', 'header', 'state_list', 'state_name_re', 'states', 'value_map', 'values']

>>> results_template.value_map
{'UPTIME': '(?P<UPTIME>.+)', 'HOSTNAME': '(?P<HOSTNAME>\\S+)', 'RUNNING_IMAGE': '(?P<RUNNING_IMAGE>\\S+)', 'CONFIG_REGISTER': '(?P<CONFIG_REGISTER>\\S+)', 'HARDWARE': '(?P<HARDWARE>\\S+\\d\\S+)', 'VERSION': '(?P<VERSION>.+?)', 'SERIAL': '(?P<SERIAL>\\S+)', 'ROMMON': '(?P<ROMMON>\\S+)'}

>>> results_template.values
[<textfsm.TextFSMValue object at 0x107c0ac90>, <textfsm.TextFSMValue object at 0x107c181d0>, <textfsm.TextFSMValue object at 0x107c18450>, <textfsm.TextFSMValue object at 0x107cf1750>, <textfsm.TextFSMValue object at 0x107cf1790>, <textfsm.TextFSMValue object at 0x107cf17d0>, <textfsm.TextFSMValue object at 0x107cf18d0>, <textfsm.TextFSMValue object at 0x107cf1950>]


You don’t need the last three commands. I just wanted to show you how to investigate the object a little which is quite easy when working in the interpreter.

This is the heart of what we are trying to do now that we’ve selected our parsing template and created an object that will parse our content and pull out the information we want.

First we open our text file of show commands. Below I create a file handle (basically a variable) to the 
show command file called content2parse and read the contents of that file into the variable content. 
You have to open the file first before you can read its contents.

>>> content2parse = open('lab-swtich-show-cmds.txt')
>>> content = content2parse.read()



Now we parse out the data we want using our results_template object (and its ParseText) method against 
our content and store the results in the parsed_results variable. As you can see, this is a list of lists. 
If you ran this against show command files from 5 different devices you can begin to see the possibilities. 
You would have a list of 5 lists with the show version information for each device.

>>> parsed_results = results_template.ParseText(content)
>>> parsed_results
[['15.2(2)E3', 'Bootstrap', 'artic-sw01', '4 days, 14 hours, 2 minutes', 'c2960s-universalk9-mz.152-2.E3.bin', ['WS-C2960S-24TS-S'], ['FOC1709W1DT'], '0xF']]



Your first inclination might be to iterate over the results but remember that its a list of lists so iterating over the results
would iterate over the only item in the "top level" list [0]. What you want is to iterate over each element of the results list
which is also a list. I know that my top level list has 1 element, the [0] element, and that list has 8 elements.  We only parsed one file which is why our list only has one element.  If I iterate 
over the [0] element of the list I get each individual bit of information.

Get the length of the top level list (should only have one element since we only parsed 1 file)
>>> len(parsed_results)
1
Get the length of that first element
>>> len(parsed_results[0])
8
>>> for item in parsed_results:
... print(item)
...
['15.2(2)E3', 'Bootstrap', 'artic-sw01', '4 days, 14 hours, 2 minutes', 'c2960s-universalk9-mz.152-2.E3.bin', ['WS-C2960S-24TS-S'], ['FOC1709W1DT'], '0xF']

>>> index = 0
>>> for item in parsed_results[0]:
... print("Element # {}: {}".format(index,item))
... index = index + 1
...
Element # 0: 15.2(2)E3
Element # 1: Bootstrap
Element # 2: artic-sw01
Element # 3: 4 days, 14 hours, 2 minutes
Element # 4: c2960s-universalk9-mz.152-2.E3.bin
Element # 5: ['WS-C2960S-24TS-S']
Element # 6: ['FOC1709W1DT']
Element # 7: 0xF
>>>


I know I have 8 elements (0-7) because i checked the length and if you do a length on the value_map you can see 
that the template is designed to get 8 pieces of information so that makes sense.

Get the length of the first list
>>> len(parsed_results[0])
8
Get the length of the value map
>>> len(results_template.value_map)
8



Finally, after all of this I wanted to get the version and hardware information out of this file and here it is:


>>> parsed_results[0][0]
'15.2(2)E3'
>>> parsed_results[0][5]
['WS-C2960S-24TS-S']
>>>

I hope these basics help you get started or keep practicing.

Here are all the main commands in sequence so you can see the flow without all of the interruptions. Also easier to copy and paste!

Python 2.7.10 (default, Feb 7 2017, 00:08:15)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import textfsm
>>> template = open('cisco_ios_show_version.template')
>>> results_template = textfsm.TextFSM(template)
>>> content2parse = open('lab-swtich-show-cmds.txt')
>>> content = content2parse.read()
>>> parsed_results = results_template.ParseText(content)
>>> parsed_results
[['15.2(2)E3', 'Bootstrap', 'artic-sw01', '4 days, 14 hours, 2 minutes', 'c2960s-universalk9-mz.152-2.E3.bin', ['WS-C2960S-24TS-S'], ['FOC1709W1DT'], '0xF']]