├── .gitignore ├── README.md └── aci-fault-doc.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aci-fault-doc 2 | ============= 3 | Author: Phillip Ferrell (phferrel@cisco.com) 4 | 5 | # Description 6 | Script to query APIC for faults and summarize corrective actions based on fault documentation on APIC 7 | 8 | # Installation 9 | 10 | ## Environment 11 | Required 12 | * Python 2.7+ 13 | * Beautiful Soup 4 (bs4 - 4.3.2) 14 | * html5lib (0.999) 15 | 16 | 17 | # Usage 18 | Script requires access to APIC to query current faultInst MOs and documentation. It also provides an option to pull fault documentation for a saved faultInst json query (/api/class/faultInst.json). 19 | 20 |
21 | usage: aci-fault-doc.py [-h] [--username USERNAME] [--pwd PWD] [--json JSON] 22 | apicUrl 23 | 24 | APIC Fault summary 25 | 26 | positional arguments: 27 | apicUrl APIC URL (http or https should be included) 28 | 29 | optional arguments: 30 | -h, --help show this help message and exit 31 | --username USERNAME username 32 | --pwd PWD password 33 | --json JSON load faults from json file (expects full json response 34 | by APIC) 35 |36 | # Example 37 |
38 | [user@localhost ~]$ ./aci-fault-doc.py http://172.18.118.5 --user admin 39 | Password: 40 | 41 | FAULT SUMMARY (grouped / sorted by # of occurrences) 42 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 43 | 44 | FAULT: F0546, NAME: fltEthpmIfPortDownNoInfra, occurred: 3 45 | FAULT: F607575, occurred: 2 46 | FAULT: F606434, occurred: 2 47 | FAULT: F1410, NAME: fltInfraClSzEqObstClusterSizeEqualization, occurred: 2 48 | FAULT: F1371, NAME: fltPconsRADeploymentStatus, occurred: 2 49 | FAULT: F1239, NAME: fltFabricLinkFailed, occurred: 2 50 | FAULT: F0475, NAME: fltTunnelIfDestUnreach, occurred: 2 51 | FAULT: F0454, NAME: fltLldpIfPortOutofService, occurred: 2 52 | FAULT: F1240, NAME: fltVzTabooConfigurationFailed, occurred: 1 53 | FAULT: F107496, occurred: 1 54 | FAULT: F0523, NAME: fltFvATgConfigurationFailed, occurred: 1 55 | FAULT: F0321, NAME: fltInfraWiNodeHealth, occurred: 1 56 | 57 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 58 | 59 | FAULT DOCUMENTATION (grouped / sorted by # of occurrences) 60 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 61 | 62 | FAULT: F0546 - 3 occurrences 63 | Fault Name: fltEthpmIfPortDownNoInfra 64 | Message: Port is down, reason operStQual , used by usage 65 | Severity: warning 66 | Type: communications 67 | Cause: port-failure 68 | Explanation: 69 | This fault occurs when a port is unconnected and is not in use for infra 70 | 71 | Recommended Action: 72 | To recover from this fault, try the following actions 73 | 74 | Check the port connectivity 75 | Remove the configuration or administratively shut the port if the port is not in use 76 | 77 | Instances (first 10): 78 | warning topology/pod-1/node-104/sys/phys-[eth1/2]/phys/fault-F0546 79 | Port is down, reason:link-failure, used by:discovery 80 | warning topology/pod-1/node-101/sys/phys-[eth1/2]/phys/fault-F0546 81 | Port is down, reason:sfp-missing, used by:discovery 82 | warning topology/pod-1/node-102/sys/phys-[eth1/2]/phys/fault-F0546 83 | Port is down, reason:sfp-missing, used by:discovery 84 | 85 | ...remaining output ommitted 86 |87 | # License 88 | 89 | Copyright (C) 2014 Cisco Systems Inc. 90 | 91 | Licensed under the Apache License, Version 2.0 (the "License"); 92 | you may not use this file except in compliance with the License. 93 | You may obtain a copy of the License at 94 | 95 | http://www.apache.org/licenses/LICENSE-2.0 96 | 97 | Unless required by applicable law or agreed to in writing, software 98 | distributed under the License is distributed on an "AS IS" BASIS, 99 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 100 | See the License for the specific language governing permissions and 101 | limitations under the License. 102 | -------------------------------------------------------------------------------- /aci-fault-doc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import argparse 4 | import requests 5 | from bs4 import BeautifulSoup, Tag, NavigableString 6 | import re 7 | import json 8 | import getpass 9 | 10 | def getApicFaults(session, apicUrl=None, name='admin', pwd=None): 11 | ''' retrieves all faultInst from APIC and returns data as dictionary ''' 12 | if apicUrl is None or name is None or pwd is None: 13 | raise Exception("Incomplete APIC login info") 14 | # login to apic 15 | loginUrl = "/api/aaaLogin.json" 16 | login_data = {"aaaUser":{"attributes":{"name":name, "pwd":pwd}}} 17 | response = session.post(apicUrl + loginUrl, data=json.dumps(login_data), verify=False).json()['imdata'][0] 18 | if 'error' in response: 19 | raise Exception(response['error']['attributes']['text']) 20 | # query apic for all faultInst MOs 21 | faultQuery = '/api/class/faultInst.json' 22 | response = session.get(apicUrl + faultQuery).json()['imdata'] 23 | if len(response) > 0 and 'error' in response[0]: 24 | raise Exception(response['error']['attributes']['text']) 25 | return response 26 | 27 | def getFaultDocumentation(session, apicUrl, faults): 28 | ''' retrieves fault documentation from APIC using requests sessions ''' 29 | docUrl = '/doc/html/' 30 | faultUrl = 'FaultMessages.html' 31 | faultsHtml = BeautifulSoup(session.get(apicUrl + docUrl + faultUrl, verify=False).content, 'html5lib') 32 | last_fault_code = None 33 | for fault_row in filter(lambda x: x.name == 'tr', faultsHtml.table.tbody.children): 34 | columns = filter(lambda x: x.name == 'td', fault_row.contents) 35 | if len(columns) > 0: 36 | if columns[0].string == 'Fault Code': 37 | last_fault_code = columns[-1].string.encode('ascii',errors='ignore') 38 | if columns[0].string == 'Fault Name': 39 | if last_fault_code in faults: 40 | faults[last_fault_code]['documentation']['url'] = columns[-1].find('a')['href'] 41 | 42 | tags_to_skip = ['a', 'br', 'p', 'tr', 'td', 'b', 'ol', 'li'] 43 | for code in faults: 44 | if 'url' in faults[code]['documentation']: 45 | faultHtml = BeautifulSoup(session.get(apicUrl + docUrl + faults[code]['documentation']['url'], verify=False).text).body 46 | for tag in faultHtml.find_all('b'): 47 | if isinstance(tag.next_sibling, basestring) and tag.next_sibling.startswith(':'): 48 | faults[code]['documentation'][tag.string] = [] 49 | last_match = '' 50 | for tag in faultHtml.descendants: 51 | if tag.name == 'b' and tag.string in faults[code]['documentation']: 52 | last_match = tag.string 53 | elif last_match != '': 54 | strings = [] 55 | if isinstance(tag, NavigableString) and tag != last_match: 56 | strings = re.sub('^:[ ]+', '', tag.encode('ascii',errors='ignore')).split('\n') 57 | elif isinstance(tag, Tag) and tag.string is not None and tag.name not in tags_to_skip: 58 | strings = tag.string.split('\n') 59 | elif isinstance(tag, Tag) and (tag.name == 'br' or tag.name == 'li'): 60 | strings = ['\n'] 61 | strings = [x.strip(' :') for x in strings if x.strip(' :') != ''] 62 | for x in strings: 63 | faults[code]['documentation'][last_match].append(x) 64 | return faults 65 | 66 | def printFaultSummary(faults): 67 | print "FAULT SUMMARY (grouped / sorted by # of occurrences)" 68 | print "+" * 80 69 | print 70 | for k, v in sorted(faults.iteritems(), key=lambda (k,v): (v['occurrences'],k), reverse=True): 71 | log = "FAULT: " + k 72 | if 'url' in v['documentation']: 73 | log += ", NAME: " + ' '.join(v['documentation']['Fault Name']).rstrip('\n').replace('\n', '\n ') 74 | print log + ", occurred: " + str(v['occurrences']) 75 | print 76 | print "+" * 80 77 | print 78 | 79 | def printFaultDocumentation(faults, documentation=False): 80 | print "FAULT DOCUMENTATION (grouped / sorted by # of occurrences)" 81 | print "+" * 80 82 | print 83 | for k, v in sorted(faults.iteritems(), key=lambda (k,v): (v['occurrences'],k), reverse=True): 84 | print "FAULT: %s - %d occurrences" % (k, v['occurrences']) 85 | if documentation: 86 | if 'url' in v['documentation']: 87 | for key in ['Fault Name', 'Message', 'Severity', 'Type', 'Cause', 'Explanation', 'Recommended Action']: 88 | if len(v['documentation'][key]) > 0: 89 | value = ' '.join(v['documentation'][key]).rstrip('\n').replace('\n', '\n ') 90 | print " %s: %s" % (key, value) 91 | else: 92 | print " DOCUMENTATION NOT AVAILABLE" 93 | print 94 | print " Instances (first 10):" 95 | for instance in sorted(v['instances'])[:10]: 96 | print " %s %s" % (instance['severity'], instance['dn']) 97 | if instance['descr'] != "": 98 | print " %s" % instance['descr'] 99 | print 100 | print "-" * 80 101 | print 102 | print "+" * 80 103 | 104 | def main(apicUrl=None, name=None, pwd=None, documentation=False, apic_faults=None): 105 | faults = {} 106 | session = requests.Session() 107 | if apic_faults is None: 108 | apic_faults = getApicFaults(session, apicUrl, name, pwd) 109 | for x in apic_faults: 110 | code = x['faultInst']['attributes']['code'] 111 | if code not in faults: 112 | faults[code] = {'occurrences':0, 'instances':[], 'documentation':{}} 113 | faults[code]['occurrences'] += 1 114 | faults[code]['instances'].append(x['faultInst']['attributes']) 115 | if documentation == True: 116 | getFaultDocumentation(session, apicUrl, faults) 117 | printFaultSummary(faults) 118 | printFaultDocumentation(faults, documentation) 119 | 120 | if __name__ == '__main__': 121 | parent_parser = argparse.ArgumentParser(description='APIC Fault summary') 122 | parent_parser.add_argument('apicUrl', help='APIC URL (http or https should be included)', type=str) 123 | parent_parser.add_argument('--username', type=str, help='username', default="admin") 124 | parent_parser.add_argument('--pwd', type=str, help='password', default=None) 125 | parent_parser.add_argument('--json', type=str, help='load faults from json file (expects full json response by APIC)') 126 | args = parent_parser.parse_args() 127 | if re.match('http', args.apicUrl) is None: 128 | args.apicUrl = 'https://' + args.apicUrl 129 | if args.pwd is None: 130 | args.pwd = getpass.getpass() 131 | apic_faults = None 132 | if args.json is not None: 133 | with open(args.json) as fp: 134 | apic_faults = json.load(fp)['imdata'] 135 | main(apicUrl=args.apicUrl, name=args.username, pwd=args.pwd, documentation=True, apic_faults=apic_faults) 136 | --------------------------------------------------------------------------------