New requirements
The initial version of the script that I wrote for getting all ports in a specific network set for 802.1x enforcment, has been doing its job very well, and providing the engineers with a means to get port configurations built out for our 'private' network and ready for day of execution. But alas, as usually happens, more politics got involved, and now we are adding to the scope of the original project...(gotta love scope creep). So the new portion, was that they wanted to include the other VRF zone that we call Open, but those ports were never originally enabled for 802.1x. So now we are looking at a couple of phases for the project, but we needed to immediately start on getting the 'open' ports configured for 'fail open' 802.1x mode or 'monitoring mode' so that we can start to collect splunk data on hosts connecting. So the new requirements for a script are as follows:
- Script must log each error and success in a written log file.
- Script must write out a configuration file for each switch.
- Script must identify ports in range command that will need to be updated.
- Script must report if there are no active ports in the Open network
- Script must show visual status as it is executing
Script
So to start, I wanted to use the bulk of the original script (why re-invent the wheel when you already have one)
So I started with the standard libraries, and then the Logging tools, since those already proved to work well in the last script:
#!/usr/bin/env python
from netmiko import ConnectHandler
import re, csv, glob, sys, getpass, logging
def setup_custom_logger(name):
formatter = logging.Formatter(fmt='%(asctime)s %(levelname)-8s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S')
handler = logging.FileHandler('log.txt', mode='w')
handler.setFormatter(formatter)
screen_handler = logging.StreamHandler(stream=sys.stdout)
screen_handler.setFormatter(formatter)
logger = logging.getLogger(name)
logger.setLevel(logging.DEBUG)
logger.addHandler(handler)
logger.addHandler(screen_handler)
return logger
logger = setup_custom_logger("PyNACOpn")
THis setup the logging function, to be used in the primary function later.
Next I decided that it would be best to define the configurations per port somewhere other than the actual GetVlans function, for clarity:
dot1X = """
authentication event fail action next-method
authentication event server alive action reinitialize
authentication host-mode multi-auth
authentication open
authentication order dot1x mab
authentication priority dot1x mab
authentication port-control auto
authentication violation restrict
mab
dot1x pae authenticator
dot1x timeout tx-period 10
!
!
"""
Notice that I used Triple Quotes ("""), that allowed me to put in a multiline str in one variable
Now on to the primary GetVlan function (notice I used the same name as the old script) ..self plagarism is a great thing, it just saves time:
def GetVlans(ip, usr, paswd):
try:
s = ConnectHandler(device_type='cisco_ios', ip=ip, username=usr, password=paswd)
hstname = s.send_command("show run | inc hostname")
vlans = s.send_command("show vlan | inc Opn")
s.disconnect()
pattern = "Et.*"
result = re.findall(pattern, vlans)
print("#"*30 + "# {} {} #".format(hstname,ip) + "#"*30)
FH = open("NAC-configs.txt", "a")
FH.write("#"*30 + "# {} {} #".format(hstname,ip) + "#"*30 + "\n")
if result == []:
print("NO PORTS ACTIVE")
FH.write("\n!!! No ports active in Open\n!\n")
FH.close()
logger.info("No ports in open on this switch")
else:
FH.write("\n!\ninterface range {}\n!".format(result[0]))
FH.write(dot1X)
FH.write("#" + "~"*75 + "\n")
print("interface range {}".format(result[0]))
print(dot1X)
FH.close()
logger.info("Success on {}".format(ip))
except:
print("\n\n#" + "*!"*35)
logger.error("Something went wrong on {}".format(ip))
print("something went wrong with {}".format(ip))
pass
This function starts with the variables ip, usr, paswd being expected to be passed to it. Next it performs in ConnectHandler from netmiko sshing into the switch (from the ip variable). Then it gets 2 commands from the switch hstname and also vlans where vlans is specific to vlans with a description containing the word Opn. Note this is based on good design practices of naming conventions, this is not always the case, and would make some of these scripts extremely harder to actually perform without a good design to being with. Next I am taking the pattern (Et.) ...which is really specific to my GNS3 lab, real world would use actual switch port identifiers like (Gi.). Then I perform a regular expression re.findall with that pattern on that vlans variable and assign it to result. Next for visual purposes on the CLI, I print the hstname and IP surrounded by 30 (#) so it comes back like this:
############################### hostname SW1 1.1.1.1 ###############################
Then the next step is something new, as I just printed to screen with the first script, this time I wanted to write out a file (separately from the log file) that showed the full configs. So I did:
FH = open("NAC-configs.txt", "a")
FH.write("#"*30 + "# {} {} #".format(hstname,ip) + "#"*30 + "\n")
Notice that I used "a" for the mode to write the file, that is so that it will open and APPEND the file each time a new switch comes in. If you just used a "w" then the file would only contain the very last switch that it loops through, becuase it is written over each time the loop goes through. Next I use the if statement to see if the regex in result returns nothing. (This just means that a particular switch didn't have any ports assigned to open). Then I simple print and write to the file NO ACTIVE PORTS, close the file and loop again. The else: statement is the ports that do contain open configs. I start the first line with the same as above describing switch hostname and IP bracketed by 30 each (#)'s. Then I write the interface range command and populate it with every interface that was pulled. Next I simply write to file the dot1X variable so that the configuration can be copied and pasted so it applies to all open ports on the switch. I then close the file and Log an info line for my log file. Of course the except: clause just prints and logs the error and does a pass.
Next I needed to use the list of IPs to loop through, and I built all of this in the previous script too:
ips = [line.rstrip("\n") for line in open("iplist.txt")]
PassWD = getpass.getpass()
for n in ips:
GetVlans(ip=n, usr='cisco', paswd=PassWD)
Then I execute the script and enter the password contained in getpass (from the script). And it iterates through my switches and generates the report "NAC-configs.txt" Which looks like the following:
############################### hostname IOU1 1.1.1.1 ###############################
!!! No ports active in Open
!
############################### hostname IOU2 2.2.2.2 ###############################
!
interface range Et2/0, Et2/1, Et2/2, Et2/3
!
authentication event fail action next-method
authentication event server alive action reinitialize
authentication host-mode multi-auth
authentication open
authentication order dot1x mab
authentication priority dot1x mab
authentication port-control auto
authentication violation restrict
mab
dot1x pae authenticator
dot1x timeout tx-period 10
!
!
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now that file can be sent to the operations team to be executed. --Carl.