├── .gitignore ├── .pylintrc ├── LICENSE.txt ├── README.md ├── apisrv ├── __init__.py ├── errors.py ├── netdb.py ├── user.py └── views.py ├── ctlsrv.py ├── cypher ├── buildfw.cyp ├── constraints.cyp └── sample-queries.cyp ├── datasources ├── allvlans.py ├── asaparse.py ├── ciscoparse.py ├── csnip.py ├── netdb-devfile.py ├── netdb-nd.py ├── update_csv_from_netdb.sh └── update_parse.sh ├── docs ├── API.md ├── About.md ├── CHANGES.md ├── CLI.md ├── CONTRIBUTING.md ├── INSTALL.md ├── Napalm.md ├── PathSample.md ├── Tutorials.md ├── VERSION ├── images │ ├── network-graph.svg │ ├── security-path2.svg │ └── svipath2.svg ├── index.md ├── netgrph-logrotate ├── netgrph.ini ├── playbooks │ ├── README.md │ ├── netgrph.yml │ ├── netgrph │ │ ├── netgrph.yml │ │ └── tasks │ │ │ ├── apt-packages.yml │ │ │ ├── apt-update.yml │ │ │ ├── clonerepo.yml │ │ │ ├── java8.yml │ │ │ ├── neo4j.yml │ │ │ └── ngpip.yml │ ├── tasks │ │ ├── apt-packages.yml │ │ ├── apt-update.yml │ │ ├── clonerepo.yml │ │ ├── java8.yml │ │ ├── neo4j.yml │ │ ├── netgrph-user.yml │ │ ├── ngpip.yml │ │ └── setup.yml │ └── test.yml └── workflow.txt ├── extra ├── apisrv-nginx ├── nsj-sample.ini ├── nsj.py ├── svipath2.svg └── wsgi-api.ini ├── mkdocs.yml ├── netgrph.py ├── nglib ├── __init__.py ├── alerts.py ├── cache_update.py ├── dev_update.py ├── exceptions.py ├── fw_update.py ├── net_update.py ├── netdb │ ├── __init__.py │ ├── ip.py │ └── switch.py ├── ngtree │ ├── __init__.py │ ├── export.py │ └── upgrade.py ├── query │ ├── __init__.py │ ├── dev.py │ ├── nNode.py │ ├── net.py │ ├── path.py │ └── vlan.py ├── report │ └── __init__.py └── vlan_update.py ├── ngreport.py ├── ngtest.py ├── ngupdate.py ├── requirements-client.txt ├── requirements.txt ├── test ├── __pycache__ │ └── testng.cpython-34-PYTEST.pyc ├── apisrv ├── bitwise.py ├── csv │ ├── allnets.csv │ ├── allvlans.csv │ ├── devices.csv │ ├── devinfo.csv │ ├── firewalls.csv │ ├── links.csv │ ├── nd.csv │ ├── supernets.csv │ └── vrfs.csv ├── first_import.sh ├── ngclient.py ├── nglib ├── pre-push.sh ├── set_neo4j_password.sh ├── test_full.sh ├── test_queries.sh ├── testapi.py ├── testrand.py └── uwsgi-api.sh └── wsgi-api.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *~ 3 | *.pyc 4 | log/*.log 5 | netgrphdev.ini 6 | netgrphtest.ini 7 | neo4j-queries.txt 8 | build-commands 9 | .vscode/ 10 | ngtest.py.bak 11 | .cache/ 12 | .DS_Store 13 | venv/ 14 | api.log 15 | api.log.* 16 | api.db 17 | newhal.crt 18 | newhal.key 19 | prod.ini 20 | test.ini 21 | nsj.ini 22 | nsj.log 23 | job.log 24 | *.cmd 25 | *.json 26 | *.log 27 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | disable=invalid-name 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Synopsis 2 | 3 | NetGrph is an abstract network model for automation, providing a unified network 4 | view across diverse network components in order to manage them as a single 5 | system via the [Neo4j Graph Database](http://neo4j.com). This enables you to 6 | navigate your traditional LAN/WAN and/or mixed SDN networks as interconnected 7 | nodes and relationships in software, all modeled via the network configurations 8 | as they exist rather than secondary sources. 9 | 10 | NetGrph can perform universal L2/L3/L4 path traversals, providing context for 11 | each layer along the path. It also serves as a VLAN and CIDR database, showing 12 | how everything is related. It scales well on even the largest networks, 13 | allowing sub-second queries across thousands of network devices. This enables 14 | the mapping of complex network relationships for discovery and automation. 15 | 16 | Data from queries can be returned as CSV, JSON, YAML, or Ascii tree-art. Network 17 | Visualizations can be created by querying the Neo4j webapp as shown below. The 18 | data model should translate for use with tools such as D3.js, vis.js or Graphwiz 19 | via both the native Neo4j API as well as NetGrph's tree data structure. 20 | 21 | All data is accessible via an API, and the lightweight netgrph client can be 22 | distributed to multiple machines. 23 | 24 | ## Graph Data Model Example: Vlan 110 -> 200 Traversal 25 | ![vlan110](https://cdn.rawgit.com/yantisj/netgrph/dev/extra/svipath2.svg) 26 | 27 | 28 | [L3 SVIs: Yellow] [L2 VLANs: Green] [Switches/Routers: Blue] 29 |
30 |
31 | 32 | ## Features 33 | 34 | * Universal Layer2 - Layer4 pathfinding between any two network devices (Full L2 path completion requires NetDB) 35 | * Path Queries can return a single path, or all ECMPs 36 | * L3 Network Database of all networks (Automated, VRF aware, and searchable) 37 | * Search for networks via CIDR or VRF/Role based filters (eg. perim:printers|thinclient, all printers and thin clients in the perim VRF) 38 | * VLAN Inventory of all VLAN instances across the network, segmented by switch domain 39 | * Maps L2 VLAN bridges across switch domains, and calculates local/global STP roots 40 | * Maps L2 paths between devices (regexs supported, eg. dc.* -> dc.* for all datacenter links) 41 | * Reports the configured VLANs and actual VLANs existing on each link for all L2 paths 42 | * Secure REST API Server and Client 43 | * High performance, low latency queries (All queries are sub-second) 44 | * Easily extendable to support mixed-vendor environnments 45 | * Ansible playbooks for a five minute install on Ubuntu 14.04/16.04 46 | 47 | ## Planned Features 48 | * Rewrite API to be more consistent 49 | * Develop a Web frontend 50 | * Integrate with Napalm for datasources 51 | 52 | ## Requirements 53 | * Python 3.4+ (recommend running via virtualenv) 54 | * Ubuntu or MacOS (should run on any Python compatible platform, but I only support these) 55 | * [Neo4j Graph Database](https://neo4j.com) and Java8 56 | * For Cisco devices, must provide stored configurations (See [Rancid](http://www.shrubbery.net/rancid/) / [Oxidized](https://github.com/ytti/oxidized)) 57 | * Requires CDP/LLDP Discovery Data. [ndcrawl](https://github.com/yantisj/ndcrawl) will output this in the proper format for NetGrph with the -ng_file option. 58 | * Third-party network devices need to be parsed into the [NetGrph CSV format](test/csv/) 59 | * Please send me any parsers you create 60 | 61 | ## Documentation 62 | 63 | * [NetGrph Read The Docs](http://netgrph.readthedocs.io/) 64 | 65 | ## Path Traversal Example 66 | ``` 67 | $ ./netgrph.py -p 10.26.72.142 10.34.72.24 68 | 69 | ┌─[ PATHs L2-L4 ] 70 | │ 71 | ├── L2 Path : abc7t1sw1 (Gi2/42) -> abc7t1sw1 (Gi1/38) 72 | ├── L3 Path : 10.26.72.0/22 -> 10.34.72.0/22 73 | ├── L4 Path : VRF:default -> FwutilFW -> VRF:utility 74 | ├── Lx Path : 10.26.72.142 -> 10.34.72.24 75 | ├── Traversal Type : All Paths 76 | │ 77 | ├─────[ SRC 10.26.72.142 04bd.88cb.xxxx abc7t1sw1(Gi2/42) [vid:260] ] 78 | │ 79 | ├───┬─[ L2-PATH abc7t1sw1 -> abcmdf1|abcmdf2 ] 80 | │ │ 81 | │ ├─────[ L2-HOP #1 abc7t1sw1(Te5/1) -> abcmdf1(Eth1/8) [pc:1->108] ] 82 | │ │ 83 | │ └─────[ L2-HOP #1 abc7t1sw1(Te6/1) -> abcmdf2(Eth1/8) [pc:1->108] ] 84 | │ 85 | ├─────[ L3GW 10.26.72.0/22 abcmdf1|abcmdf2 ] 86 | │ 87 | ├───┬─[ L3-PATH 10.26.72.0/22 -> 10.25.11.0/24 ] 88 | │ │ 89 | │ ├───┬─[ L3-HOP #1 abcmdf1(10.23.74.11) -> core1(10.23.74.10) [vid:2074] ] 90 | │ │ │ 91 | │ │ └─────[ L2-HOP #1 abcmdf1(Eth2/26) -> core1(Eth7/27) ] 92 | │ │ 93 | │ ├───┬─[ L3-HOP #1 abcmdf1(10.23.74.21) -> core2(10.23.74.20) [vid:3074] ] 94 | │ │ │ 95 | │ │ └─────[ L2-HOP #1 abcmdf1(Eth3/8) -> core2(Eth4/25) ] 96 | │ │ 97 | │ ├───┬─[ L3-HOP #1 abcmdf2(10.23.78.11) -> core1(10.23.78.10) [vid:2078] ] 98 | │ │ │ 99 | │ │ └─────[ L2-HOP #1 abcmdf2(Eth2/26) -> core1(Eth8/25) ] 100 | │ │ 101 | │ └───┬─[ L3-HOP #1 abcmdf2(10.23.78.21) -> core2(10.23.78.20) [vid:3078] ] 102 | │ │ 103 | │ └─────[ L2-HOP #1 abcmdf2(Eth3/8) -> core2(Eth8/25) ] 104 | │ 105 | ├─────[ L4GW 10.25.11.0/24 [rtr: vid:1601 vrf:default] ] 106 | │ 107 | ├─────[ L4FW FwutilFW ] 108 | │ 109 | ├─────[ L4GW 10.25.12.0/24 [rtr: vid:1602 vrf:utility] ] 110 | │ 111 | ├───┬─[ L3-PATH 10.25.12.0/24 -> 10.34.72.0/22 ] 112 | │ │ 113 | │ ├───┬─[ L3-HOP #1 core1(10.23.74.10) -> abcmdf1(10.23.74.11) [vid:2461] ] 114 | │ │ │ 115 | │ │ └─────[ L2-HOP #1 core1(Eth7/27) -> abcmdf1(Eth2/26) ] 116 | │ │ 117 | │ ├───┬─[ L3-HOP #1 core1(10.23.78.10) -> abcmdf2(10.23.78.11) [vid:2462] ] 118 | │ │ │ 119 | │ │ └─────[ L2-HOP #1 core1(Eth8/25) -> abcmdf2(Eth2/26) ] 120 | │ │ 121 | │ ├───┬─[ L3-HOP #1 core2(10.23.74.20) -> abcmdf1(10.23.74.21) [vid:3461] ] 122 | │ │ │ 123 | │ │ └─────[ L2-HOP #1 core2(Eth4/25) -> abcmdf1(Eth3/8) ] 124 | │ │ 125 | │ └───┬─[ L3-HOP #1 core2(10.23.78.20) -> abcmdf2(10.23.78.21) [vid:3462] ] 126 | │ │ 127 | │ └─────[ L2-HOP #1 core2(Eth8/25) -> abcmdf2(Eth3/8) ] 128 | │ 129 | ├─────[ L3GW 10.34.72.0/22 abcmdf1|abcmdf2 ] 130 | │ 131 | ├───┬─[ L2-PATH abcmdf1|abcmdf2 -> abc7t1sw1 ] 132 | │ │ 133 | │ ├─────[ L2-HOP #1 abcmdf1(Eth1/8) -> abc7t1sw1(Te5/1) [pc:108->1] ] 134 | │ │ 135 | │ └─────[ L2-HOP #1 abcmdf2(Eth1/8) -> abc7t1sw1(Te6/1) [pc:108->1] ] 136 | │ 137 | └─────[ DST 10.34.72.24 000a.b004.xxxx abc7t1sw1(Gi1/38) [vid:340] ] 138 | ``` 139 | 140 | ## Installation 141 | 142 | See the [Install Instructions](docs/INSTALL.md) 143 | 144 | ## Support 145 | 146 | See refer to the documentation first: [NetGrph Read The 147 | Docs](http://netgrph.readthedocs.io/) 148 | 149 | You can open an issue via GitHub, or if you would like to speak with me 150 | directly, I monitor the #netgrph channel the [networktocode slack 151 | group](https://networktocode.herokuapp.com/). Please try and contact me there 152 | for any interactive support. 153 | 154 | ## Contributions 155 | 156 | Please see the [Contributions](docs/CONTRIBUTING.md) document in docs for 157 | information about how you can contribute back to NetGrph. 158 | 159 | ## Contributors 160 | * Jonathan Yantis ([yantisj](https://github.com/yantisj)) 161 | 162 | ## License 163 | NetGrph is licensed under the GNU AGPLv3 License. 164 | -------------------------------------------------------------------------------- /apisrv/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 "Jonathan Yantis" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3, 6 | # as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | # As a special exception, the copyright holders give permission to link the 17 | # code of portions of this program with the OpenSSL library under certain 18 | # conditions as described in each individual source file and distribute 19 | # linked combinations including the program with the OpenSSL library. You 20 | # must comply with the GNU Affero General Public License in all respects 21 | # for all of the code used other than as permitted herein. If you modify 22 | # file(s) with this exception, you may extend this exception to your 23 | # version of the file(s), but you are not obligated to do so. If you do not 24 | # wish to do so, delete this exception statement from your version. If you 25 | # delete this exception statement from all source files in the program, 26 | # then also delete it in the license file. 27 | # 28 | # 29 | """ 30 | Flask API Server 31 | """ 32 | import builtins 33 | import sys 34 | import os 35 | import re 36 | import logging 37 | from logging.handlers import RotatingFileHandler 38 | import configparser 39 | from flask import Flask, jsonify, request, g, make_response 40 | from flask_httpauth import HTTPBasicAuth 41 | from flask_limiter import Limiter 42 | from flask_limiter.util import get_remote_address 43 | from flask_sqlalchemy import SQLAlchemy 44 | import nglib 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | # Populated from config file 49 | debug = 0 50 | 51 | # Flask Limits for Safety 52 | flask_limits = ["100000 per day", "5000 per hour", "600 per minute"] 53 | 54 | # Initialize Configuration 55 | config_file = None 56 | if 'NGLIB_config_file' in os.environ: 57 | config_file = os.environ['NGLIB_config_file'] 58 | else: 59 | config_file = builtins.apisrv_CONFIG 60 | config = configparser.ConfigParser() 61 | config.read(config_file) 62 | 63 | # Check for sane config file 64 | if 'apisrv' not in config: 65 | print("Could not parse config file: " + config_file) 66 | sys.exit(1) 67 | 68 | # Logging Configuration, default level INFO 69 | logger = logging.getLogger('') 70 | logger.setLevel(logging.INFO) 71 | lformat = logging.Formatter('%(asctime)s %(name)s:%(levelname)s: %(message)s') 72 | 73 | # Debug mode 74 | if 'debug' in config['apisrv'] and int(config['apisrv']['debug']) != 0: 75 | debug = int(config['apisrv']['debug']) 76 | logger.setLevel(logging.DEBUG) 77 | logging.debug('Enabled Debug mode') 78 | 79 | # Enable logging to file if configured 80 | if 'logfile' in config['apisrv']: 81 | lfh = RotatingFileHandler(config['apisrv']['logfile'], maxBytes=(1048576*5), backupCount=3) 82 | lfh.setFormatter(lformat) 83 | logger.addHandler(lfh) 84 | 85 | # STDOUT Logging defaults to Warning 86 | if not debug: 87 | lsh = logging.StreamHandler(sys.stdout) 88 | lsh.setFormatter(lformat) 89 | lsh.setLevel(logging.WARNING) 90 | logger.addHandler(lsh) 91 | 92 | # Create Flask APP 93 | app = Flask(__name__) 94 | app.config.from_object(__name__) 95 | app.config.update(dict( 96 | DATABASE=os.path.join(app.root_path, config['apisrv']['database']), 97 | SQLALCHEMY_DATABASE_URI='sqlite:///' + os.path.join(app.root_path, config['apisrv']['database']), 98 | SQLALCHEMY_TRACK_MODIFICATIONS=False, 99 | #SECRET_KEY='development key', 100 | #USERNAME='dbuser', 101 | #PASSWORD='dbpass' 102 | )) 103 | 104 | # Auth module 105 | auth = HTTPBasicAuth() 106 | 107 | # Database module 108 | db = SQLAlchemy(app) 109 | 110 | # Apply Rate limiting 111 | limiter = Limiter( 112 | app, 113 | key_func=get_remote_address, 114 | global_limits=flask_limits 115 | ) 116 | 117 | def get_bolt_db(): 118 | """Store nglib database handle under thread""" 119 | bdb = getattr(g, '_boltdb', None) 120 | if bdb is None: 121 | bdb = g._boltdb = nglib.get_bolt_db() 122 | return bdb 123 | 124 | def get_py2neo_db(): 125 | """Store nglib database handle under thread""" 126 | pydb = getattr(g, '_py2neodb', None) 127 | if pydb is None: 128 | pydb = g._py2neodb = nglib.get_py2neo_db() 129 | return pydb 130 | 131 | @app.before_request 132 | def init_db(): 133 | """Initialize Library on each request""" 134 | logging.debug('Getting Neo4j Database Connections') 135 | nglib.verbose = debug 136 | nglib.init_nglib(config_file, initdb=False) 137 | nglib.bolt_ses = get_bolt_db() 138 | g.neo4j_db_bolt = nglib.bolt_ses 139 | nglib.py2neo_ses = get_py2neo_db() 140 | g.neo4j_db_py2neo = nglib.py2neo_ses 141 | 142 | @app.teardown_appcontext 143 | def close_db(error): 144 | """Closes the database again at the end of the request.""" 145 | if hasattr(g, 'sqlite_db'): 146 | g.sqlite_db.close() 147 | 148 | bolt_ses = getattr(g, 'neo4j_db_bolt', None) 149 | if bolt_ses is not None: 150 | logger.debug('Closing Neo4j Database Connection') 151 | g.neo4j_db = None 152 | nglib.bolt_ses = None 153 | bolt_ses.last_result = None 154 | bolt_ses.close() 155 | 156 | py2neo_ses = getattr(g, 'neo4j_db_py2neo', None) 157 | if py2neo_ses is not None: 158 | logger.debug('Closing Neo4j Database Connection') 159 | g.neo4j_db = None 160 | nglib.py2neo_ses = None 161 | py2neo_ses.last_result = None 162 | py2neo_ses = None 163 | 164 | 165 | def upgrade_api(ngt, version): 166 | 'Upgrade the output of NGT API data' 167 | 168 | nt = ngt 169 | 170 | if version == 'v2': 171 | nt = nglib.ngtree.upgrade.upgrade_ngt_v2(ngt) 172 | return nt 173 | 174 | def version_chk(version, versions=['v1.1', 'v2']): 175 | 'Make sure version in versions' 176 | 177 | if version not in versions: 178 | return jsonify(errors.json_error('API Version Error', 'Version ' \ 179 | + version + ' not supported on this endpoint')) 180 | 181 | # Safe circular imports per Flask guide 182 | import apisrv.errors 183 | import apisrv.views 184 | import apisrv.user 185 | import apisrv.netdb 186 | 187 | 188 | -------------------------------------------------------------------------------- /apisrv/errors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 "Jonathan Yantis" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3, 6 | # as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | # As a special exception, the copyright holders give permission to link the 17 | # code of portions of this program with the OpenSSL library under certain 18 | # conditions as described in each individual source file and distribute 19 | # linked combinations including the program with the OpenSSL library. You 20 | # must comply with the GNU Affero General Public License in all respects 21 | # for all of the code used other than as permitted herein. If you modify 22 | # file(s) with this exception, you may extend this exception to your 23 | # version of the file(s), but you are not obligated to do so. If you do not 24 | # wish to do so, delete this exception statement from your version. If you 25 | # delete this exception statement from all source files in the program, 26 | # then also delete it in the license file. 27 | # 28 | """ 29 | API Errors Jsonified 30 | """ 31 | import logging 32 | from flask import Flask, jsonify, request, g, make_response 33 | from apisrv import app, auth 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | @app.errorhandler(404) 38 | def not_found(error=None): 39 | message = { 40 | 'status': 404, 41 | 'message': 'Not Found: ' + request.url, 42 | } 43 | resp = jsonify(message) 44 | resp.status_code = 404 45 | return resp 46 | 47 | @app.errorhandler(429) 48 | def ratelimit_handler(e): 49 | return make_response( 50 | jsonify(error="ratelimit exceeded %s" % e.description) 51 | , 429 52 | ) 53 | 54 | @auth.error_handler 55 | def auth_failed(error=None): 56 | message = { 57 | 'status': 401, 58 | 'message': 'Authentication Failed: ' + request.url 59 | } 60 | resp = jsonify(message) 61 | resp.status_code = 401 62 | 63 | return resp 64 | 65 | @app.errorhandler(400) 66 | def bad_request(error): 67 | print("Bad Request") 68 | message = { 69 | 'status': 400, 70 | 'message': 'Bad Request: ' + request.url, 71 | } 72 | resp = jsonify(message) 73 | resp.status_code = 400 74 | return resp 75 | 76 | def json_error(error, message, code=404): 77 | 78 | ngerror = dict() 79 | ngerror['Name'] = error 80 | ngerror['_type'] = 'Error' 81 | ngerror['message'] = error + ': ' + message 82 | ngerror['status'] = code 83 | 84 | return ngerror 85 | -------------------------------------------------------------------------------- /apisrv/netdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 "Jonathan Yantis" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3, 6 | # as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | # As a special exception, the copyright holders give permission to link the 17 | # code of portions of this program with the OpenSSL library under certain 18 | # conditions as described in each individual source file and distribute 19 | # linked combinations including the program with the OpenSSL library. You 20 | # must comply with the GNU Affero General Public License in all respects 21 | # for all of the code used other than as permitted herein. If you modify 22 | # file(s) with this exception, you may extend this exception to your 23 | # version of the file(s), but you are not obligated to do so. If you do not 24 | # wish to do so, delete this exception statement from your version. If you 25 | # delete this exception statement from all source files in the program, 26 | # then also delete it in the license file. 27 | # 28 | """ 29 | NetDB API Views 30 | """ 31 | import logging 32 | import nglib 33 | import nglib.query 34 | import nglib.report 35 | import nglib.netdb.switch 36 | from nglib.exceptions import ResultError 37 | from flask import jsonify, request 38 | from apisrv import app, auth, config, errors, upgrade_api, version_chk 39 | 40 | # Setup 41 | logger = logging.getLogger(__name__) 42 | app_name = config['apisrv']['app_name'] 43 | 44 | # Device Queries 45 | @app.route('/netdb/api//table/arp', methods=['GET']) 46 | @app.route('/api//table/arp', methods=['GET']) 47 | @auth.login_required 48 | def get_arptable(ver): 49 | """ Get ARP Tables from NetDB 50 | """ 51 | 52 | error = version_chk(ver, ['v1.0', 'v2']) 53 | if error: 54 | return error 55 | 56 | router = None 57 | hours = 1 58 | 59 | if 'router' in request.args: 60 | router = request.args['router'] 61 | router = router.replace('*', '%') 62 | else: 63 | return jsonify(errors.json_error('InputError', 'router= is required')) 64 | 65 | if 'hours' in request.args: 66 | hours = int(request.args['hours']) 67 | 68 | 69 | try: 70 | return jsonify(upgrade_api(nglib.netdb.ip.arp(router=router, hours=hours), ver)) 71 | except ResultError as e: 72 | return jsonify(errors.json_error(e.expression, e.message)) 73 | 74 | @app.route('/netdb/api//table/mac', methods=['GET']) 75 | @app.route('/api//table/mac', methods=['GET']) 76 | @auth.login_required 77 | def get_mactable(ver): 78 | """ Get the MAC Table from NetDB 79 | """ 80 | 81 | error = version_chk(ver, ['v1.0', 'v2']) 82 | if error: 83 | return error 84 | 85 | switch = None 86 | port = '%' 87 | hours = 1 88 | 89 | if 'switch' in request.args: 90 | switch = request.args['switch'] 91 | switch = switch.replace('*', '%') 92 | else: 93 | return jsonify(errors.json_error('InputError', 'switch= is required')) 94 | if 'port' in request.args: 95 | port = request.args['port'] 96 | port = port.replace('*', '%') 97 | print("port", port) 98 | 99 | if 'hours' in request.args: 100 | hours = int(request.args['hours']) 101 | 102 | try: 103 | return jsonify(upgrade_api(nglib.netdb.switch.mac(switch=switch, port=port, hours=hours), ver)) 104 | except ResultError as e: 105 | return jsonify(errors.json_error(e.expression, e.message)) 106 | 107 | 108 | @app.route('/netdb/api//table/mac//count', methods=['GET']) 109 | @app.route('/api//table/mac//count', methods=['GET']) 110 | @auth.login_required 111 | def get_mac_count(ver, switch): 112 | """ Get the MAC Table count from NetDB 113 | """ 114 | 115 | error = version_chk(ver, ['v1.0', 'v2']) 116 | if error: 117 | return error 118 | 119 | hours = 1 120 | switch = switch.replace('*', '%') 121 | 122 | if 'hours' in request.args: 123 | hours = int(request.args['hours']) 124 | 125 | try: 126 | return jsonify(upgrade_api(nglib.netdb.switch.count(switch=switch, hours=hours), ver)) 127 | except ResultError as e: 128 | return jsonify(errors.json_error(e.expression, e.message)) 129 | -------------------------------------------------------------------------------- /apisrv/user.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2016 "Jonathan Yantis" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3, 6 | # as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | # As a special exception, the copyright holders give permission to link the 17 | # code of portions of this program with the OpenSSL library under certain 18 | # conditions as described in each individual source file and distribute 19 | # linked combinations including the program with the OpenSSL library. You 20 | # must comply with the GNU Affero General Public License in all respects 21 | # for all of the code used other than as permitted herein. If you modify 22 | # file(s) with this exception, you may extend this exception to your 23 | # version of the file(s), but you are not obligated to do so. If you do not 24 | # wish to do so, delete this exception statement from your version. If you 25 | # delete this exception statement from all source files in the program, 26 | # then also delete it in the license file. 27 | # 28 | """ 29 | API User Functions 30 | """ 31 | import sys 32 | import logging 33 | import time 34 | from passlib.hash import sha256_crypt 35 | 36 | import sqlite3 37 | from flask_sqlalchemy import SQLAlchemy 38 | from apisrv import auth, config, db 39 | from sqlalchemy.exc import OperationalError 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | class User(db.Model): 44 | """ SQL User Model""" 45 | id = db.Column(db.Integer, primary_key=True) 46 | username = db.Column(db.String(80), unique=True) 47 | password = db.Column(db.String(120), unique=True) 48 | role = db.Column(db.String(40)) 49 | 50 | def __init__(self, username, password, role='default'): 51 | self.username = username 52 | self.password = password 53 | self.role = role 54 | 55 | def __repr__(self): 56 | return ''.format(self.username) 57 | 58 | @auth.verify_password 59 | def verify_password(username, password): 60 | """API Password Verification""" 61 | 62 | if authenticate_user(username, password): 63 | return True 64 | return False 65 | 66 | def get_hash(password): 67 | """ Returns a new SHA256 Hash for a password (lower rounds for speed)""" 68 | 69 | phash = sha256_crypt.encrypt(password, rounds=100000) 70 | return phash 71 | 72 | def authenticate_user(username, password): 73 | """ Authenticate a user """ 74 | 75 | try: 76 | user = User.query.filter_by(username=username).first() 77 | except OperationalError: 78 | db.create_all() 79 | user = User.query.filter_by(username=username).first() 80 | 81 | 82 | authenticated = False 83 | 84 | if user: 85 | authenticated = sha256_crypt.verify(password, user.password) 86 | else: 87 | time.sleep(1) 88 | logger.info("Authentication Error: User not found in DB: %s", username) 89 | return False 90 | 91 | if authenticated: 92 | logger.debug("Successfully Authenticated user: %s", username) 93 | else: 94 | logger.info("Authentication Failed: %s", username) 95 | 96 | return authenticated 97 | 98 | 99 | def add_user(username, password): 100 | """ 101 | Add a new user to the database 102 | """ 103 | 104 | user = User.query.filter_by(username=username).first() 105 | 106 | if user: 107 | #print("Error: User already exists in DB", file=sys.stderr) 108 | raise Exception("Error: User already exists in DB") 109 | elif len(password) < 6: 110 | print("Error: Password must be 6 or more characters", file=sys.stderr) 111 | exit(1) 112 | else: 113 | logger.info("Adding new user to the database: %s", username) 114 | 115 | phash = get_hash(password) 116 | 117 | newuser = User(username, phash) 118 | db.session.add(newuser) 119 | db.session.commit() 120 | 121 | return phash 122 | 123 | def update_password(username, password): 124 | """ Update password for user """ 125 | 126 | user = User.query.filter_by(username=username).first() 127 | 128 | if len(password) < 6: 129 | print("Error: Password must be 6 or more characters", file=sys.stderr) 130 | exit(1) 131 | elif user: 132 | logger.info("Updating password for user: %s", username) 133 | 134 | phash = get_hash(password) 135 | 136 | user.password = phash 137 | db.session.commit() 138 | 139 | return phash 140 | else: 141 | 142 | print("Error: User does not exists in DB", file=sys.stderr) 143 | exit(1) 144 | 145 | def del_user(username): 146 | """ Delete a user from the database """ 147 | 148 | user = User.query.filter_by(username=username).first() 149 | 150 | if user: 151 | logger.info("Deleting user: %s", username) 152 | 153 | db.session.delete(user) 154 | db.session.commit() 155 | 156 | return True 157 | else: 158 | 159 | print("Error: User does not exists in DB", file=sys.stderr) 160 | exit(1) 161 | 162 | -------------------------------------------------------------------------------- /ctlsrv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2016 "Jonathan Yantis" 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU Affero General Public License, version 3, 6 | # as published by the Free Software Foundation. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | # 16 | # As a special exception, the copyright holders give permission to link the 17 | # code of portions of this program with the OpenSSL library under certain 18 | # conditions as described in each individual source file and distribute 19 | # linked combinations including the program with the OpenSSL library. You 20 | # must comply with the GNU Affero General Public License in all respects 21 | # for all of the code used other than as permitted herein. If you modify 22 | # file(s) with this exception, you may extend this exception to your 23 | # version of the file(s), but you are not obligated to do so. If you do not 24 | # wish to do so, delete this exception statement from your version. If you 25 | # delete this exception statement from all source files in the program, 26 | # then also delete it in the license file. 27 | # 28 | """ 29 | Manage the API Server 30 | """ 31 | import builtins 32 | import os 33 | import re 34 | import getpass 35 | import argparse 36 | import logging 37 | 38 | logger = logging.getLogger('ctlsrv') 39 | 40 | parser = argparse.ArgumentParser() 41 | 42 | parser = argparse.ArgumentParser(prog='ctlsrv.py', 43 | description='Manage the API Server') 44 | 45 | parser.add_argument('--run', help='Run the API Server', action="store_true") 46 | parser.add_argument("--adduser", metavar="user", help="Add API User to DB", type=str) 47 | parser.add_argument("--newpass", metavar="user", help="Update Password", type=str) 48 | parser.add_argument("--testuser", metavar="user", help="Test Authentication", type=str) 49 | parser.add_argument("--deluser", metavar="user", help="Delete API User", type=str) 50 | parser.add_argument("--initdb", help="Initialize the Database", action="store_true") 51 | parser.add_argument("--debug", help="Set debugging level", type=int) 52 | parser.add_argument("-v", help="Verbose Output", action="store_true") 53 | 54 | args = parser.parse_args() 55 | 56 | 57 | # Default Config File Location 58 | config_file = '/etc/netgrph.ini' 59 | alt_config = './docs/netgrph.ini' 60 | 61 | # Test/Dev Config 62 | dirname = os.path.dirname(os.path.realpath(__file__)) 63 | if re.search(r'\/dev$', dirname): 64 | config_file = 'netgrphdev.ini' 65 | elif re.search(r'\/test$', dirname): 66 | config_file = "netgrphdev.ini" 67 | 68 | # Test configuration exists 69 | if not os.path.exists(config_file): 70 | if not os.path.exists(alt_config): 71 | raise Exception("Configuration File not found", config_file) 72 | else: 73 | config_file = alt_config 74 | 75 | # Import API 76 | builtins.apisrv_CONFIG = config_file 77 | import apisrv 78 | 79 | verbose = 0 80 | if args.v: 81 | verbose = 1 82 | if args.debug: 83 | verbose = args.debug 84 | 85 | # Initialize the Database 86 | if args.initdb: 87 | apisrv.db.create_all() 88 | apisrv.db.session.commit() 89 | 90 | # Run the API Server 91 | elif args.run: 92 | 93 | builtins.apisrv_CONFIG = config_file 94 | from apisrv import app, debug, config 95 | 96 | # Production server in HTTPS Mode 97 | if config['apisrv']['https'] != '0': 98 | context = (config['apisrv']['ssl_crt'], config['apisrv']['ssl_key']) 99 | app.run(host='0.0.0.0', port=int(config['apisrv']['port']), \ 100 | ssl_context=context, threaded=True, debug=debug) 101 | 102 | # Localhost development server 103 | else: 104 | logger.warning("HTTPS is not configured, defaulting to localhost only") 105 | app.run(debug=debug, port=int(config['apisrv']['port'])) 106 | 107 | # Add user to DB 108 | elif args.adduser: 109 | 110 | passwd = getpass.getpass('Password:') 111 | verify = getpass.getpass('Verify Password:') 112 | 113 | if passwd == verify: 114 | phash = apisrv.user.add_user(args.adduser, passwd) 115 | if phash: 116 | print("Successfully Added User to Database") 117 | else: 118 | print("Error: Could not Add User to Database") 119 | else: 120 | print("Error: Passwords do not match") 121 | 122 | # Update User Password 123 | elif args.newpass: 124 | passwd = getpass.getpass('New Password:') 125 | verify = getpass.getpass('Verify Password:') 126 | 127 | if passwd == verify: 128 | phash = apisrv.user.update_password(args.newpass, passwd) 129 | if phash: 130 | print("Successfully Updated Password") 131 | else: 132 | print("Error: Could not Update Password") 133 | else: 134 | print("Error: Passwords do not match") 135 | 136 | # Delete a User 137 | elif args.deluser: 138 | ucheck = apisrv.user.del_user(args.deluser) 139 | 140 | if ucheck: 141 | print("Successfully Deleted User") 142 | else: 143 | print("Username not found in DB") 144 | 145 | # Test Authentication 146 | elif args.testuser: 147 | passwd = getpass.getpass('Password:') 148 | phash = apisrv.user.authenticate_user(args.testuser, passwd) 149 | if phash: 150 | print("Successfully Authenticated") 151 | else: 152 | print("Authentication Failed") 153 | else: 154 | parser.print_help() 155 | print() -------------------------------------------------------------------------------- /cypher/buildfw.cyp: -------------------------------------------------------------------------------- 1 | -- 2 | -- Manual Cypher Topology Commands 3 | -- Will not be purged by cache since they're missing a timestamp 4 | -- 5 | -- Create a Default network and link it to the outside VRF 6 | MERGE (n:Network {vrfcidr:"outside-0.0.0.0/0"}) SET n += {name:"outside", cidr:"0.0.0.0/0", vid:'0', desc:"Default Route", vrf:"outside", gateway:"0.0.0.0"} 7 | MATCH (n:Network {cidr:"0.0.0.0/0"}), (v:VRF {name:"outside"}) MERGE (n)-[e:VRF_IN]->(v) 8 | MATCH (s:Switch {name:"ExternalFW"}), (n:Network {vrfcidr:"outside-0.0.0.0/0"}) MERGE (n)-[e:ROUTED_BY {vrf:"OUTSIDE"}]->(s) 9 | 10 | -- Create a SWITCHED_FW and insert it between two networks on different VRFs 11 | MERGE (fw:Switch:FW {name:'ExternalFW', type:"PaloAlto", hostname:"pa-fw-l2fw.domain.com", logIndex:"fwlogs"}); 12 | MATCH (fw:Switch:FW {name:'ExternalFW'}), (n:Network {vid:"1095"}) MERGE (n)-[e:SWITCHED_FW]->(fw) 13 | MATCH (fw:Switch:FW {name:'ExternalFW'}), (n:Network {vid:"1099"}) MERGE (n)-[e:SWITCHED_FW]->(fw) 14 | -------------------------------------------------------------------------------- /cypher/constraints.cyp: -------------------------------------------------------------------------------- 1 | -- Create constraints on DB for consistency and performance 2 | --CREATE Garbage 3 | CREATE CONSTRAINT ON (n:Network) ASSERT n.vrfcidr IS UNIQUE 4 | CREATE CONSTRAINT ON (s:Switch) ASSERT s.name IS UNIQUE 5 | CREATE CONSTRAINT ON (v:VRF) ASSERT v.name IS UNIQUE 6 | CREATE CONSTRAINT ON (v:VLAN) ASSERT v.name IS UNIQUE 7 | CREATE CONSTRAINT ON (e:Entity) ASSERT e.name IS UNIQUE 8 | CREATE INDEX ON :Network(cidr) 9 | CREATE INDEX ON :VLAN(vid) 10 | CREATE INDEX ON :VLAN(mgmt) 11 | -------------------------------------------------------------------------------- /cypher/sample-queries.cyp: -------------------------------------------------------------------------------- 1 | --- Switched path between two switches 2 | MATCH (ss:Switch {name:"abc2sw1"}), (ds:Switch {name:"xyz2sw1"}), 3 | sp = allShortestPaths((ss)-[:NEI*0..5]-(ds)) RETURN sp 4 | 5 | -- All downstream neighbors from here 6 | MATCH(s:Switch {name:"core1"})-[e:NEI*0..5]->(rs), (s)-[p:NEI]-(parent) RETURN e,p 7 | 8 | -- All networks in the default table 9 | match (v:VRF {name:"default"})<-[e:VRF_IN]-(n:Network) return e 10 | 11 | -- Checkout the nglib/query modules 12 | -------------------------------------------------------------------------------- /datasources/asaparse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | # Copyright (c) 2016 "Jonathan Yantis" 5 | # 6 | # This file is a part of NetGrph. 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License, version 3, 10 | # as published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | # 20 | # As a special exception, the copyright holders give permission to link the 21 | # code of portions of this program with the OpenSSL library under certain 22 | # conditions as described in each individual source file and distribute 23 | # linked combinations including the program with the OpenSSL library. You 24 | # must comply with the GNU Affero General Public License in all respects 25 | # for all of the code used other than as permitted herein. If you modify 26 | # file(s) with this exception, you may extend this exception to your 27 | # version of the file(s), but you are not obligated to do so. If you do not 28 | # wish to do so, delete this exception statement from your version. If you 29 | # delete this exception statement from all source files in the program, 30 | # then also delete it in the license file. 31 | # 32 | # 33 | """Parse ASA Config Files for NetGrph Data""" 34 | 35 | import sys 36 | import os 37 | import re 38 | import argparse 39 | import socket 40 | import csv 41 | import ipaddress 42 | import logging 43 | from ciscoconfparse import CiscoConfParse 44 | 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | # Config Options 49 | 50 | fwdir = "/tftpboot/asafw/" 51 | extension = ".cfg" 52 | 53 | # Argument Parser 54 | parser = argparse.ArgumentParser() 55 | parser = argparse.ArgumentParser(description='Generate Cisco ASA Configuration Details') 56 | 57 | parser.add_argument("-fd", metavar='asafwfile', help="ASA FW Device file", type=str) 58 | parser.add_argument("-ffile", metavar='outfile', help="NetGrph Firewall File", type=str) 59 | parser.add_argument("-debug", help="Set debugging level", type=int) 60 | 61 | # Argument Reference 62 | args = parser.parse_args() 63 | 64 | DEBUG = 0 65 | if args.debug: DEBUG = args.debug 66 | 67 | 68 | def load_fwfile(fw_file): 69 | """Import ASA CSV Configuration File""" 70 | 71 | logger.debug("Importing Firewalls from " + fw_file) 72 | 73 | f = open(fw_file, 'r') 74 | fwdb = csv.DictReader(f) 75 | 76 | fwimport = [] 77 | 78 | for en in fwdb: 79 | asafile = fwdir + en['FW'] + extension 80 | 81 | if os.path.exists(asafile): 82 | logger.debug("Found ASA File: " + asafile) 83 | 84 | fwints = process_asa_file(asafile, en) 85 | fwimport.append(fwints) 86 | 87 | else: 88 | logger.error("Could not find ASA file: " + asafile) 89 | 90 | return fwimport 91 | 92 | 93 | def process_asa_file(asafile, en): 94 | """Go through individual ASA Configuration Looking for Interfaces to import""" 95 | 96 | f = open(asafile) 97 | 98 | fwints = [] 99 | instance = en['FW'] 100 | 101 | instance = instance.title() 102 | instance = instance + "FW" 103 | 104 | inside = None 105 | intVlan = None 106 | desc = None 107 | seclevel = None 108 | ip = None 109 | 110 | for line in f: 111 | line = line.strip() 112 | ivlan = re.search(r'interface (Vlan\d+)', line) 113 | rdesc = re.search(r'^description ', line) 114 | rsec = re.search(r'^security-level (\d+)', line) 115 | 116 | if inside and re.search('^!', line): 117 | logger.debug("FW Found Ints: %s , %s , %s , %s , %s , %s , %s", 118 | instance, intVlan, desc, seclevel, ip, en['hostname'], 119 | en['logIndex']) 120 | 121 | fwints.append("{0},{1},{2},{3},{4},{5},{6}".format( 122 | instance, intVlan, desc, seclevel, ip, en['hostname'], en['logIndex'])) 123 | 124 | inside = None 125 | intVlan = None 126 | desc = None 127 | seclevel = None 128 | ip = None 129 | 130 | if ivlan: 131 | inside = 1 132 | intVlan = ivlan.group(1) 133 | elif inside and rdesc: 134 | desc = line 135 | desc = desc.replace('description ', '') 136 | elif inside and rsec: 137 | seclevel = rsec.group(1) 138 | 139 | # Return found ints 140 | return fwints 141 | 142 | 143 | # Write results to file 144 | def save_fw_file(data, out_file): 145 | 146 | save = open(out_file, "w") 147 | print("Name,Interface,Description,Security-Level,IP,Hostname,Log-Index", file=save) 148 | 149 | for fw in data: 150 | for entry in fw: 151 | print(entry, file=save) 152 | save.close() 153 | 154 | 155 | ## Process Arguments to generate output 156 | # Got VLAN Range and Switch Name 157 | if args.fd and args.ffile: 158 | 159 | loglevel = logging.INFO 160 | 161 | if DEBUG: 162 | loglevel = logging.DEBUG 163 | 164 | logging.basicConfig(format='%(asctime)s %(name)s:%(levelname)s: %(message)s', 165 | stream=sys.stdout, level=loglevel) 166 | 167 | # Load FW Data 168 | fwimport = load_fwfile(args.fd) 169 | 170 | # Save FW Data 171 | save_fw_file(fwimport, args.ffile) 172 | 173 | 174 | # Print Help 175 | else: 176 | parser.print_help() 177 | print() 178 | -------------------------------------------------------------------------------- /datasources/csnip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Generate Config Snippets from Cisco Devices 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | # 33 | # 34 | import sys 35 | import re 36 | import argparse 37 | import csv 38 | from ciscoconfparse import CiscoConfParse 39 | 40 | # Config Options 41 | conf_dir = "/tftpboot/" 42 | extension = "-confg" 43 | 44 | # Argument Parser 45 | parser = argparse.ArgumentParser() 46 | parser = argparse.ArgumentParser(description='Generate Cisco Snippets') 47 | parser.add_argument("switch", help="Switch to get snippet from", 48 | type=str) 49 | parser.add_argument("-int", metavar="interface", 50 | help="Interface (switchport or VLAN) to pull config from", 51 | type=str) 52 | parser.add_argument('-match', metavar='string', 53 | help='Match a string on an interface', 54 | type=str) 55 | parser.add_argument("-debug", help="Set debugging level", type=int) 56 | 57 | # Argument Reference 58 | args = parser.parse_args() 59 | 60 | 61 | DEBUG = 0 62 | if args.debug: DEBUG = args.debug 63 | 64 | 65 | def get_int(switch, interface): 66 | """ Get a config snippet from a device """ 67 | 68 | conf_file = conf_dir + switch + extension 69 | 70 | interface = expand_port(interface) 71 | 72 | try: 73 | parse = CiscoConfParse(conf_file) 74 | except: 75 | print("Error: could not load config for ", conf_file) 76 | sys.exit(1) 77 | 78 | search_int = "^interface " + interface + "$" 79 | 80 | 81 | 82 | if args.match: 83 | m = parse.find_objects_w_child(parentspec=search_int, 84 | childspec=args.match) 85 | if m: 86 | print(args.switch + ',' + args.int + ',' + args.match) 87 | 88 | else: 89 | iface = parse.find_all_children(search_int) 90 | if iface: 91 | print('!' + switch + ' ' + interface) 92 | for line in iface: 93 | print(line) 94 | if iface: 95 | print('!') 96 | 97 | 98 | def normalize_port(port): 99 | """Normalize Ports (GigabitEthernet1/1 -> Gi1/1)""" 100 | 101 | 102 | port = port.replace('interface ', '') 103 | port = port.replace('TenGigabitEthernet', 'Te') 104 | port = port.replace('GigabitEthernet', 'Gi') 105 | port = port.replace('FastEthernet', 'Fa') 106 | port = port.replace('Ethernet', 'Eth') 107 | port = port.replace('v', 'Vlan') 108 | port = port.replace('vlan', 'Vlan') 109 | 110 | return port 111 | 112 | def expand_port(port): 113 | 114 | port = port.title() 115 | 116 | port = re.sub(r'^Gi(\d+)', r'GigabitEthernet\1', port) 117 | port = re.sub(r'^Te(\d+)', r'TenGigabitEthernet\1', port) 118 | port = re.sub(r'^Fa(\d+)', r'FastEthernet\1', port) 119 | port = re.sub(r'^Eth(\d+)', r'Ethernet\1', port) 120 | port = re.sub(r'^E(\d+)', r'Ethernet\1', port) 121 | port = re.sub(r'^Po(\d+)', r'Port-channel\1', port) 122 | 123 | return port 124 | 125 | ## Process Arguments to generate output 126 | # Got VLAN Range and Switch Name 127 | if args.switch and args.int: 128 | 129 | get_int(args.switch, args.int) 130 | 131 | # Print Help 132 | else: 133 | parser.print_help() 134 | print() 135 | -------------------------------------------------------------------------------- /datasources/netdb-devfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Generate Device File from NetDB 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | # 33 | # 34 | import sys 35 | import re 36 | import argparse 37 | import socket 38 | 39 | # Config Options 40 | configDir = "/opt/netdb/data/" 41 | 42 | # Argument Parser 43 | parser = argparse.ArgumentParser() 44 | parser = argparse.ArgumentParser(description='Generate Devicefile Database') 45 | parser.add_argument("-df", metavar='neighbor_file', help="devicefile.csv from NetDB", type=str) 46 | parser.add_argument("-of", metavar='outfile', help="Output File for CSV", type=str) 47 | parser.add_argument("-debug", help="Set debugging level", type=int) 48 | 49 | # Argument Reference 50 | args = parser.parse_args() 51 | 52 | DEBUG = 0 53 | if args.debug: DEBUG = args.debug 54 | 55 | def load_nd_file(fileName): 56 | 57 | df = open(fileName, 'r') 58 | 59 | devdata = [] 60 | 61 | for line in df: 62 | if re.search('^\#', line ): 63 | continue 64 | 65 | line = line.strip() 66 | en = line.split(',') 67 | 68 | device = get_entry(en[0].split('.')) 69 | fqdn = en[0] 70 | mgmt = None 71 | dtype = "Switch" 72 | platform = 'cisco_ios' 73 | 74 | for e in en: 75 | # Device Flags 76 | mg = re.search('mgmtgroup\=(\w+)', e) 77 | arp = re.search('^(arp|netdbarp)$', e) 78 | standby = re.search('^(standby)$', e) 79 | p = re.search('devtype\=(\w+)', e) 80 | if mg: 81 | mgmt = mg.group(1) 82 | elif arp: 83 | dtype = "Primary" 84 | elif standby: 85 | dtype = "Standby" 86 | elif p: 87 | #print(p.group(1)) 88 | platform = tr_devtype(p.group(1)) 89 | 90 | #print(router) 91 | #print(en) 92 | 93 | dev = ("{0},{1},{2},{3},{4}").format(device,fqdn,mgmt,dtype,platform) 94 | devdata.append(dev) 95 | 96 | return devdata 97 | 98 | def tr_devtype(dt): 99 | """Translation NetDB devtype to Netmiko""" 100 | if dt == 'nxos': 101 | dt = 'cisco_nxos' 102 | elif dt == 'asa': 103 | dt = 'cisco_asa' 104 | 105 | return dt 106 | 107 | 108 | def save_nd_data(ndata): 109 | 110 | save = open(args.of, "w") 111 | print("Device,FQDN,MgmtGroup,Type,Platform", file=save) 112 | 113 | print(*ndata, sep='\n', file=save) 114 | save.close() 115 | 116 | def get_entry(l,pos=0): 117 | return l[pos] 118 | 119 | ## Process Arguments to generate output 120 | if args.df and args.of: 121 | 122 | devdata = load_nd_file(args.df) 123 | save_nd_data(devdata) 124 | 125 | # Print Help 126 | else: 127 | parser.print_help() 128 | print() 129 | -------------------------------------------------------------------------------- /datasources/netdb-nd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Generate Neighbor Data from NetDB 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | # 33 | # 34 | import sys 35 | import re 36 | import argparse 37 | import socket 38 | 39 | # Config Options 40 | configDir = "/opt/netdb/data/" 41 | 42 | # Argument Parser 43 | parser = argparse.ArgumentParser() 44 | parser = argparse.ArgumentParser(description='Generate ND Database') 45 | parser.add_argument("-nf", metavar='neighbor_file', help="Neighbor Data from NetDB", type=str) 46 | parser.add_argument("-of", metavar='outfile', help="Output File for CSV", type=str) 47 | parser.add_argument("-debug", help="Set debugging level", type=int) 48 | 49 | # Argument Reference 50 | args = parser.parse_args() 51 | 52 | DEBUG = 0 53 | if args.debug: DEBUG = args.debug 54 | 55 | def load_nd_file(fileName): 56 | 57 | nd = open(fileName, 'r') 58 | 59 | ndata = [] 60 | 61 | for line in nd: 62 | if re.search('^\#', line ): 63 | continue 64 | 65 | line = line.strip() 66 | en = line.split(',') 67 | localName = en[0] 68 | localPort = en[1] 69 | remoteName = get_entry(en[2].split('.')) 70 | remotePort = en[6] 71 | 72 | # Save data to list 73 | ndline = "{0},{1},{2},{3}".format(localName,localPort,remoteName,remotePort) 74 | ndata.append(ndline) 75 | 76 | return ndata 77 | 78 | def save_nd_data(ndata): 79 | 80 | save = open(args.of, "w") 81 | print("LocalName,LocalPort,RemoteName,RemotePort", file=save) 82 | 83 | print(*ndata, sep='\n', file=save) 84 | save.close() 85 | 86 | def get_entry(l,pos=0): 87 | return l[pos] 88 | 89 | ## Process Arguments to generate output 90 | if args.nf and args.of: 91 | 92 | ndata = load_nd_file(args.nf) 93 | save_nd_data(ndata) 94 | 95 | # Print Help 96 | else: 97 | parser.print_help() 98 | print() 99 | -------------------------------------------------------------------------------- /datasources/update_csv_from_netdb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Change to the current netgrph folder 3 | export NGHOME=/home/netgrph/netgrph 4 | export NGCSV=/home/netgrph/csv 5 | export NETDBDATA=/opt/netdb/data 6 | 7 | cd $NGHOME/datasources/ 8 | ./netdb-nd.py -nf $NETDBDATA/nd.csv -of $NGCSV/nd.csv 9 | ./netdb-devfile.py -df $NETDBDATA/devicelist.csv -of $NGCSV/devices.csv 10 | -------------------------------------------------------------------------------- /datasources/update_parse.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | export NGHOME=/home/netgrph/netgrph 3 | export NGCSV=/home/netgrph/csv 4 | export NETDBDATA=/opt/netdb/data 5 | 6 | $NGHOME/datasources/ciscoparse.py -vr 1-4096 -ivr 0-4096 -df $NGCSV/devices.csv -dfile $NGCSV/devinfo.csv -mf /scripts/versions.txt -lfile $NGCSV/links.csv -ifile $NGCSV/allnets.csv -vfile $NGCSV/allvlans.csv 7 | $NGHOME/datasources/asaparse.py -fd $NGCSV/asafirewalls.csv -debug 0 -ffile $NGCSV/firewalls.csv 8 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # REST API Reference 2 | 3 | * Version 1.1 of the API allows granular querying of the data model via REST Calls 4 | * Most query options support Neo4j/JAVA style regexes 5 | * **Base URL** 6 | * __`/netgrph/api/v1.1`__ 7 | 8 | ## Setup 9 | * Configure the [api] section in netgrph.ini 10 | * Enable HTTPS using valid certs for serving API calls beyond localhost 11 | * Setup Process 12 | * Initialize the SQL Lite database: ```./ctlsrv.py --initdb``` 13 | * Add an API User: ```./ctlsrv --adduser testuser``` 14 | * Run the Server: ```./ctlsrv.py --run``` 15 | * Test a Query: ```curl -u testuser:testpass http://localhost:4096/netgrph/api/v1.1/devs``` 16 | 17 | # Examples 18 | 19 | ### Switch path 20 | 21 | ``` 22 | $ curl -u testuser:testpass 'http://localhost:4096/netgrph/api/v1.0/spath?src=mdcmdf&dst=core1' 23 | { 24 | "Distance": 1, 25 | "Links": 1, 26 | "Name": "mdcmdf -> core1", 27 | "Search Depth": "20", 28 | "Traversal Type": "All Paths", 29 | "_ccount": 1, 30 | "_type": "L2-PATH", 31 | "data": [ 32 | { 33 | "From Channel": "0", 34 | "From Model": "WS-C3850-48U", 35 | "From Port": "Te1/1/4", 36 | "From Switch": "mdcmdf", 37 | "Link VLANs": "1246,2200,2314,2320,2324,2365,2376", 38 | "Link rVLANs": "1246,2200,2314,2320,2324,2365,2376", 39 | "Name": "#1 mdcmdf(Te1/1/4) -> core1(Eth10/21)", 40 | "Native VLAN": "2200", 41 | "To Channel": "0", 42 | "To Model": "Nexus7000 C7010", 43 | "To Port": "Eth10/21", 44 | "To Switch": "core1", 45 | "_ccount": 0, 46 | "_cversion": "03.06.04.E RELEASE SOFTWARE (fc2)", 47 | "_pversion": "Version 6.2(16)", 48 | "_reverse": 1, 49 | "_rvlans": "1246,2200,2314,2320,2324,2365,2376", 50 | "_type": "L2-HOP", 51 | "data": [], 52 | "distance": 1 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | ### Networks on a filter (guest VRF with guest access role) 59 | 60 | ``` 61 | curl -u testuser:testpass 'http://localhost:4096/netgrph/api/v1.1/nets?filter=guest:nac-guest' 62 | { 63 | "Count": 23, 64 | "Filter": "guest:nac-guest", 65 | "Name": "Networks", 66 | "_ccount": 23, 67 | "_type": "NET", 68 | "data": [ 69 | { 70 | "CIDR": "10.29.2.0/23", 71 | "Description": "None", 72 | "Gateway": "10.29.2.1", 73 | "Location": null, 74 | "Name": "10.29.2.0/23", 75 | "NetRole": "nac-guest", 76 | "Router": "mdcmdf", 77 | "SecurityLevel": "10", 78 | "StandbyRouter": null, 79 | "VLAN": "270", 80 | "VRF": "guest", 81 | "_type": "CIDR", 82 | "vrfcidr": "guest-10.29.2.0/23" 83 | }, 84 | ``` 85 | ___ 86 | 87 | # Device Calls 88 | 89 | 90 | Device Queries allow you to query the API for all devices, regexes and gather 91 | specific information from individual devices. 92 | 93 | * __URLs__ 94 | * __`/devs`__: List all Devices 95 | * __`/devs/{device}`__: Specific Device 96 | * __`/devs/{device}/vlans`__: Device VLANs 97 | * __`/devs/{device}/neighbors`__: Device Neighbors 98 | * __`/devs/{device}/nets`__: Device Networks 99 | 100 | * **Method:** 101 | * `GET` 102 | 103 | * __URL Parameters__ 104 | * __`group`__: Restrict on device group regex 105 | * __`search`__: Restrict on device name regex 106 | * __`full`__: Return full device reports (non-truncated and slower) 107 | 108 | * **Success Response:** 109 | * **Code:** 200
110 | **Content:** `{ Name : Report, _type : "VIDs" }` 111 | 112 | * **Error Response:** 113 | * **Code:** 401
114 | **Content:** `{ message : "Request Error" }` 115 | 116 | ___ 117 | 118 | # Network Calls 119 | 120 | Network Queries allow you to query for networks based on CIDRs, VRF, Role and 121 | Group filters. 122 | 123 | * __URLs__ 124 | * __`/nets`__: List of All Networks 125 | * __`/nets?group=X&cidr=X`__: Networks filtered on group and cidr 126 | * __`/nets?ip=X`__: Most Specific CIDR from IP 127 | 128 | * **Method:** 129 | * `GET` 130 | 131 | * __URL Parameters__ 132 | * __`group`__: Restrict on device group regex 133 | * __`cidr`__: Restrict on device name regex (optionally replace /24 mask with -24) 134 | * __`filter`__: Filter via NetGrph vrf:role syntax 135 | * __`ip`__: Find most specific CIDR for IP 136 | 137 | * **Success Response:** 138 | * **Code:** 200
139 | **Content:** `{ Name : Report, _type : "Networks" }` 140 | 141 | * **Error Response:** 142 | * **Code:** 401
143 | **Content:** `{ message : "Request Error" }` 144 | 145 | ___ 146 | 147 | # VLAN Calls 148 | 149 | VLAN Queries return lists of all VLANs, specific VLAN trees and VLANs for groups 150 | 151 | * __URLs__ 152 | * __`/vlans`__: Retrieve All VLANs 153 | * __`/vlans/`__: Retrieve Specific VLAN in VID or VNAME Format 154 | 155 | * __URL Parameters__ 156 | * __`group`__: VLANs in a Management Group 157 | * __`range`__: VLAN Range eg. 1-1005 158 | 159 | * **Success Response:** 160 | * **Code:** 200
161 | **Content:** `{ Name : Report, _type : "VLANs" }` 162 | 163 | * **Error Response:** 164 | * **Code:** 401
165 | **Content:** `{ message : "Request Error" }` 166 | 167 | ___ 168 | 169 | # Path Calls 170 | 171 | Path queries allow you to do L2-L4 traversals between any two points on the network. 172 | 173 | * __URLs__ 174 | * __`/path?src=[ip]&dst=[ip]`__: Universal L2-L4 Path Query 175 | * __`/spath?src=[switch]&dst[switch.*]`__: Switch paths using a regex 176 | * __`/rpath?src=[ip]&dst=[ip]&vrf=pci`__: L3 Path inside a VRF 177 | * __`/fpath?src=[ip]&dst=[ip]`__: L4 Path Query 178 | 179 | * __URL Parameters__ 180 | * __`src`__(required): Source of Path Query (regex support on switch paths) 181 | * __`dst`__(required): Destination of Path Query 182 | * __`onepath`__(set True): Only show one path, no ECMP 183 | * __`depth`__: Depth of Graph Search (default 20) 184 | * __`vrf`__: VRF for Routed Queries 185 | * __Note__: src and dst support regexes on switch paths 186 | 187 | * **Method:** 188 | * `GET` 189 | 190 | * **Success Response:** 191 | * **Code:** 200
192 | **Content:** `{ Name : Report, _type : "PATH" }` 193 | 194 | * **Error Response:** 195 | * **Code:** 401
196 | **Content:** `{ message : "Request Error" }` 197 | 198 | -------------------------------------------------------------------------------- /docs/About.md: -------------------------------------------------------------------------------- 1 | ## About 2 | 3 | NetGrph was created to serve as a singular network model for automation to 4 | replace the patchwork inherent to our current automation solutions. The API is 5 | developing to provide strong automation capabilities for our network team, 6 | as well as our server, security, and desktop teams. 7 | 8 | ## Data Model 9 | ### Routed SVI Paths from Vlan 110 to 200 10 | ![vlan110](https://dl.dropboxusercontent.com/u/73454/svipath2.svg) 11 | 12 | 13 | [L3 SVIs: Yellow] [L2 VLANs: Green] [Switches/Routers: Blue] 14 |
15 |
16 | 17 | ### Security Path from Vlan 696 --> 641 across multiple L2/L3 Firewalls 18 | ![fwpath](https://dl.dropboxusercontent.com/u/73454/security-path2.svg) 19 | 20 | 21 | [Networks: Yellow] [VRFs: Green] [Firewalls: Blue] 22 | 23 |
24 | 25 | ### Neighbor Tree from the Core out to a distance of 3 26 | 27 | NEI Tree 28 | 29 | 30 | 31 |
32 | 33 | ## Performance 34 | 35 | On our network containing 700+ routers/switches, 2000+ SVIs and 10,000+ VLAN to 36 | switch mappings, performance on almost all netgrph queries is below 150ms. Full 37 | ngreport queries can take longer, but it's for creating network-wide reports. 38 | 39 | The database update time on our localhost server with SSDs is 150sec for a 40 | complete network refresh. When importing to an unoptimized VM on a remote 41 | server, the import time is 5min. The network import speed could be dramatically 42 | increased if rewritten using the Bolt driver, which is planned. 43 | 44 | ## Dependencies 45 | 46 | NetGrph is a Python3.4+ application, but could be back-ported to Python2.7 47 | fairly easily. It relies on both the py2neo and neo4j bolt driver (install via 48 | pip3). The plan is to eventually convert all driver code to the bolt driver as 49 | it matures. 50 | 51 | NetGrph requires [CSV files](test/csv/) with all of your Switches/Routers, Networks, 52 | VLANs, and CDP/LLDP Neighbors in order to be multi-vendor compatible. I provide 53 | IOS and NXOS configuration parsers, as well as a sample network topology to play 54 | with. For Cisco-based networks, a generic CDP/LLDP mapper is all that's missing. 55 | NetGrph is compatible with NetDB Neighbor Data: 56 | (https://sourceforge.net/projects/netdbtracking/). Once stable, I'll package a 57 | bundled NetDB OVA with NetGrph included. 58 | 59 | If anyone has a dependable LLDP/CDP walker they recommend (I'm sure there are 60 | many), please contact me. This only needs src_switch,src_port,dst_switch,dst_port. 61 | I have tested a few different open-source packages, but have yet to find a suitable 62 | tool that is reliable enough. 63 | 64 | NetGrph was developed on Ubuntu 14.04 LTS, tested on 16.04, and should be 65 | compatible with other versions of linux. I highly recommend using Ubuntu at this 66 | early stage for better support. I also plan to create an ansible build script in 67 | the next few months. It also works just find on MacOS but there are no Ansible or 68 | install instructions for that OS. 69 | 70 | ## Planned Features 71 | 72 | * Add configuration snippets for each hop on traverals 73 | * Import all Network ACL's for L4 analysis 74 | * Improve NetDB integration with universal search 75 | * Implement Dijkstra's Algorithm for cost-based path traversals (database plugin) 76 | * Simple Web Interface for Path Traversals and report generation 77 | * Statseeker integration for including graphs/errors in reports 78 | 79 | ## Future 80 | 81 | I am open to expanding NetGrph for the needs of MPLS networks and other 82 | network/security domains where appropriate. The application was written to be 83 | generic and approachable for use with both SDN and existing networks. 84 | 85 | I have also added some lightweight integration with my existing [NetDB 86 | application](http://netdbtracking.sourceforge.net), but that will be both 87 | focused and optional. If you manage to create any new parsers or integrate with 88 | other vendor APIs, please contribute your code back. 89 | 90 | I would like to add a GUI, but at this time I'm focussed on using the 91 | application to automate tasks. In theory, it should be easy to create [D3 92 | visualizations](https://github.com/d3/d3/wiki/Gallery) from the NGTree 93 | data-structures. If anyone manages to create a simple GUI or use this 94 | application to create some interesting visualizations, I'd be happy to help and 95 | would love to see the results. 96 | 97 | I will not be expanding the application to be a full-blown NMS, since there are 98 | many tools that accomplish this. If you manage to expand the core modeling 99 | functionality and think it should be included in the main codebase, I'd like to 100 | consider including it here. 101 | 102 | See the [CONTRIBUTING](CONTRIBUTING.md) document for more information. 103 | 104 | ## Support 105 | 106 | You can open an issue via GitHub, or if you would like to speak with me 107 | directly, I monitor the #netgrph channel the [networktocode slack 108 | group](https://networktocode.herokuapp.com/). Please try and contact me there 109 | for any interactive support. 110 | 111 | ## Contributions 112 | 113 | Please see the [Contributions](CONTRIBUTING.md) document in docs for 114 | information about how you can contribute back to NetGrph. 115 | 116 | ## Contributors 117 | * Jonathan Yantis ([yantisj](https://github.com/yantisj)) 118 | 119 | ## License 120 | NetGrph is licensed under the GNU AGPLv3 License. 121 | -------------------------------------------------------------------------------- /docs/CHANGES.md: -------------------------------------------------------------------------------- 1 | 2 | ### v0.7.3 3 | - Added vrf and dev to ngreport 4 | - Added nfilter to netgrph to pass in custom filters at runtime 5 | - Cleaned up debug netgrph.ini handling 6 | - VLAN Group printing follows terminal size 7 | 8 | ### v0.7.0 9 | - Created public repo 10 | - Added lots of documentation 11 | - Cleaned up config files 12 | -------------------------------------------------------------------------------- /docs/CLI.md: -------------------------------------------------------------------------------- 1 | # CLI Documentation 2 | 3 | NetGrph has two CLI query programs, netgrph.py and ngreport.py. The first is for 4 | queries such as networks, paths etc. The second is for network-wide reporting on 5 | devices, VRFs etc. 6 | 7 | Data can be returned in several outputs via the --output switch 8 | 9 | ## Query Options 10 | ``` 11 | 12 | usage: netgrph [-h] [-ip] [-net] [-nlist] [-dev] [-fpath src] [-rpath src] 13 | [-spath src] [-group] [-vrange 1[-4096]] [-vid] [-vtree] 14 | [-output TREE] [--conf file] [--debug DEBUG] [--verbose] 15 | search 16 | 17 | Query the NetGrph Database 18 | 19 | positional arguments: 20 | search Search the NetGrph Database (Wildcard Default) 21 | 22 | optional arguments: 23 | -h, --help show this help message and exit 24 | -ip Network Details for an IP 25 | -net All networks within a CIDR (eg. 10.0.0.0/8) 26 | -nlist Get all networks in an alert group 27 | -dev Get the Details for a Device (Switch/Router/FW) 28 | -fpath src Security Path between -fp src dst 29 | -rpath src Routed Path between -rp IP/CIDR1 IP/CIDR2 30 | -spath src Switched Path between -sp sw1 sw2 (Neo4j Regex) 31 | -group Get VLANs for a Management Group 32 | -vrange 1[-4096] VLAN Range (default 1-1999) 33 | -vid VLAN ID Search 34 | -vtree Get the VLAN Tree for a VNAME 35 | -output TREE Return Format: TREE, TABLE, CSV, JSON, YAML 36 | --conf file Alternate Config File 37 | --debug DEBUG Set debugging level 38 | --verbose Verbose Output 39 | 40 | Examples: netgrph 10.1.1.1 (Free Search for IP), netgrph -net 10.1.1.0/24 41 | (Search for CIDR), netgrph -group MDC (VLAN Database Search), netgrph -fp 42 | 10.1.1.1 10.2.2.1 (Firewall Path Search) 43 | 44 | ``` 45 |
46 | 47 | ## Report Options 48 | ``` 49 | $ ngreport -h 50 | usage: ngreport [-h] [-vrf name] [-vrfs] [-vlans] [-vrange 1[-4096]] [-dev .*] 51 | [-output TREE] [-empty] [--conf file] [--debug DEBUG] 52 | [--verbose] 53 | 54 | Generate Reports from NetGrph 55 | 56 | optional arguments: 57 | -h, --help show this help message and exit 58 | -vrf name Generate a Report on a VRF 59 | -vrfs VRF Report on all VRFs 60 | -vlans VLAN ID Report (combine with -vra and -empty) 61 | -vrange 1[-4096] VLAN Range (default 1-1999) 62 | -dev .* Report on Network Devices in regex 63 | -output TREE Return Format: TREE, TABLE, CSV, JSON, YAML 64 | -empty Only Return Empty VLANs (requires NetDB) 65 | --conf file Alternate Config File 66 | --debug DEBUG Set debugging level 67 | --verbose Verbose Output 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributions 2 | 3 | These are the early days of NetGrph, but I fully plan to support this program 4 | going forward and am looking for contributors. I am also the primary maintainer 5 | of the [Network Tracking Database 6 | (NetDB)](http://netdbtracking.sourceforge.net/), which is in production on some 7 | extremely large networks. NetGrph is currently serving as our internal model for 8 | network automation tasks, and we hope others find it useful as well. It's 9 | primary purpose is to do work at this time, from automation to troubleshooting 10 | tasks. 11 | 12 | ### Data Sources 13 | 14 | NetGrph is short on data sources at this time, so if you manage to generate the 15 | necessary [CSV files](csv/) for your network, I will most likely accept them in 16 | to the repository without much trouble. All source data is CSV based to make it 17 | easy for others to contribute to the project. Please explore those files for 18 | understanding how to get your network data in to the program. 19 | 20 | I am considering using [Napalm](https://github.com/napalm-automation/napalm) in 21 | the future for a more robust data gathering source, but at the moment I will be 22 | busy integrating this application in to our automation and troubleshooting test 23 | suite. If you have some ideas or would like to work on this, please contact me 24 | on Slack. I am open to any and all solutions at the moment, as long as they are 25 | simple, maintainable and based off of other open-source network tools. 26 | 27 | ### Core Library Changes 28 | 29 | [nglib](nglib/) is the core library of the application. Currently, the only code 30 | using the library are the CLI scripts netgrph, ngupdate and ngreport. This 31 | application was written with APIs in mind though, so the next phase after 32 | finishing up all the reporting and user functionality via the CLI will be to 33 | create an API via Flask. I will also be creating some user facing 34 | troubleshooting GUI tools, but at this time, the GUI plans are quite simple. 35 | That said, there is no reason via nglib and a framework like Flask, that this 36 | could not be developed in to a full fledged GUI app. 37 | 38 | If you submit a pull request on the core library, please make sure you put your 39 | code through the pylint process and follow the existing code style. If you have 40 | any concerns about whether your code will be accepted, please contact me on 41 | Slack. I'd like to see contributions beyond simple data sources as long as they 42 | don't break the data model or complicate the maintainability of the code. This 43 | is very much an experimental program at this time, so I'm open to new directions 44 | from here. 45 | -------------------------------------------------------------------------------- /docs/INSTALL.md: -------------------------------------------------------------------------------- 1 | ## Install Notes 2 | 3 | Netgrph was built on Ubuntu 14.04 LTS but should be portable to any other Python 4 | 3.4+ system. I have done testing on Ubuntu 16.04 and MacOS, and I highly 5 | recommend Ubuntu trusty or xenial for support purposes. I provide ansible 6 | scripts for an easy install on a base Ubuntu system, and will be creating a 7 | Docker file before long. 8 | 9 | ### Ansible Install 10 | - See the README.md in docs/playbooks/ 11 | - Install ansible on your machine, and point the playbook towards localhost as 12 | documented, or a remote host if you are familiar with Ansible. 13 | 14 | ### Manual Install Instructions 15 | 16 | - For testing, you can install everything but the database under your user on any system 17 | - If you do not have root access to your system, use virtualenv to satisfy the pip requirements 18 | 19 | - Clone the github repository to ~/ 20 | - Install [Neo4j Community Edition](http://neo4j.com/download/) (available as a package for Ubuntu but requires java8+) 21 | ``` 22 | ## Python system requirements includes testing libraries 23 | sudo apt-get install python3-pip python3-pytest python3-logilab-common 24 | 25 | # Java and Neo4j 26 | sudo add-apt-repository ppa:webupd8team/java 27 | 28 | sudo apt-get update 29 | echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | sudo /usr/bin/debconf-set-selections 30 | sudo apt-get install oracle-java8-installer 31 | wget -O - https://debian.neo4j.org/neotechnology.gpg.key | sudo apt-key add - 32 | echo 'deb http://debian.neo4j.org/repo stable/' >/tmp/neo4j.list 33 | sudo mv /tmp/neo4j.list /etc/apt/sources.list.d 34 | sudo apt-get update 35 | sudo apt-get install neo4j 36 | ``` 37 | 38 | - To allow remote connections to neo4j, open this file and uncomment this line: 39 | ``` 40 | sudo vim /etc/neo4j/neo4j.conf 41 | dbms.connector.http.address=0.0.0.0:7474 42 | ``` 43 | 44 | - To install the software on a server separate from the database, uncomment this: 45 | ``` 46 | dbms.connector.bolt.address=0.0.0.0:7687 47 | ``` 48 | 49 | - Browse to the database http://localhost:7474 and set a password (takes a few minutes to startup). 50 | - Configure ~/netgrph/docs/netgraph.ini with your DB password. 51 | - Install json, yaml, pymysql, py2neo and neo4j-bolt packages: 52 | ``` 53 | sudo pip3 install -r requirements.txt 54 | ``` 55 | - Import the sample network: `./test/first_import.sh` 56 | - Run `./ngupdate.py -v -full` a couple more times to seed the network topology 57 | - Browse to the database web interface and run `MATCH (s:Switch) RETURN s` 58 | - Try out some sample commands 59 | 60 | ## Sample commands for use with test data 61 | ``` 62 | ./netgrph.py -nf all 63 | ./netgrph.py -nf all -o tree 64 | ./ngreport.py -dev ".*" 65 | ./ngreport.py -vrf "perim|default" 66 | ./netgrph.py abc4mdf 67 | ./netgrph.py abc4mdf -o json 68 | ./netgrph.py abc4mdf -o yaml 69 | ./netgrph.py -sp abc2sw1 xyz2sw1 70 | ./netgrph.py -sp abc.* xyz.* -o csv 71 | ./netgrph.py -rp 10.1.120.50 10.7.206.0/23 72 | ./netgrph.py 120 73 | ./netgrph.py 1246 74 | ./netgrph.py -fp 10.1.120.50 8.8.8.8 75 | ./netgrph.py -nlist test_group 76 | ./netgrph.py -nlist test_group -o tree 77 | ./netgrph.py -group ABC 78 | ./ngreport.py -vrf "perim|default" 79 | ./ngreport.py -vlans 80 | ``` 81 | 82 | ## Sample Reporting (to be expanded) 83 | ./ngreport.py -vlans 84 | 85 | 86 | ### Production Install 87 | 88 | - I would add a netgrph user to your system and clone the repo under that user 89 | - If you have a Cisco network, see the datasources/ directory file for parsing information 90 | - At the top of the files, you can configure your config locations and extensions. 91 | - See the update_parse.py and update_csv_from_netdb.sh scripts to see how I gather my data from netdb and stored configs. 92 | - Consider installing NetDB for access to the Neighbor Discovery Data (I hope to find an alternative soon) 93 | - Consider using something like Rancid or [Oxidized](https://github.com/ytti/oxidized) for gathering configurations from your devices. 94 | - Create symlinks to netgrph.py, ngupdate.py and ngreport.py under /usr/local/bin/ 95 | - Make sure all users can read your config file and consider installing it under /etc/netgrph.ini (preferred) 96 | - Consider changing your log location to /var/log/nglib.log and make sure all users have access to write to this file 97 | - Schedule cron jobs under the netgrph user to update regularly (Full NetDB integration example) 98 | ``` 99 | 01,16,31,46 * * * * /home/yantisj/netgrph/prod/datasources/update_csv_from_netdb.sh 100 | 22 * * * * /home/yantisj/netgrph/prod/datasources/update_parse.sh 101 | 02,17,32,48 * * * * /home/yantisj/netgrph/prod/ngupdate.py -full 102 | 12 * * * * /home/yantisj/netgrph/prod/ngupdate.py -unetdb 103 | ``` 104 | - Periodically clear node caches as they expire (adding -v will only show you what's expired) 105 | ``` 106 | ngupdate --clearEdges --hours 12 107 | ngupdate --clearNodes --hours 12 108 | ``` 109 | 110 | ## Adding firewalls and third-party devices 111 | - Examine the csv files in test/csv/ to understand the required datasources for importing third-party data 112 | - Examine the cyp/buildfw.cyp for understanding how to insert a L2 firewall between VRFs 113 | - Examine the cyp/sample-queries.cyp to start querying the Neo4j database directly for data 114 | -------------------------------------------------------------------------------- /docs/Napalm.md: -------------------------------------------------------------------------------- 1 | # Napalm Support 2 | 3 | ## NetGrph 4 | 5 | ### Supported data 6 | * Routed Interfaces 7 | * LLDP Neighbors 8 | * Model, Version, Serial, Uptime 9 | 10 | ### Missing Data 11 | 12 | #### L3 13 | * VRF 14 | * Virtual gateways (HSRP/VRRP) 15 | 16 | #### L2 17 | * L2 VLAN List 18 | * VLAN Names 19 | * STP priority configuration 20 | 21 | #### Interfaces 22 | * Port-channel membership (match mac address?) 23 | * Trunked VLAN configuration (1-4096 etc) 24 | * Native VLAN ID 25 | 26 | ## NetDB Support 27 | * MAC Table 28 | * ARP Table 29 | * Interface Table 30 | -------------------------------------------------------------------------------- /docs/VERSION: -------------------------------------------------------------------------------- 1 | v0.9.1 2 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to the NetGrph Documentation 2 | 3 | NetGrph is an abstract network model for automation, providing a unified network 4 | view across diverse network components in order to manage them as a single 5 | system via the [Neo4j Graph Database](http://neo4j.com). This enables you to 6 | navigate your traditional LAN/WAN and/or mixed SDN networks as interconnected 7 | nodes and relationships in software, all modeled via the network configurations 8 | as they exist rather than secondary sources. 9 | 10 | NetGrph can perform universal L2/L3/L4 path traversals, providing context for 11 | each layer along the path. It also serves as a VLAN and CIDR database, showing 12 | how everything is related. It scales well on even the largest networks, 13 | allowing sub-second queries across thousands of network devices. This enables 14 | the mapping of complex network relationships for discovery and automation. 15 | 16 | Data from queries can be returned as CSV, JSON, YAML, or Ascii tree-art. Network 17 | Visualizations can be created by querying the Neo4j webapp as shown below. The 18 | data model should translate for use with tools such as D3.js, vis.js or Graphwiz 19 | via both the native Neo4j API as well as NetGrph's tree data structure. 20 | 21 | All data is accessible via an API, and the lightweight netgrph client can be 22 | distributed to multiple machines. 23 | 24 | ## Graph Data Model Example: Vlan 110 -> 200 Traversal 25 | ![vlan110](https://dl.dropboxusercontent.com/u/73454/svipath2.svg) 26 | 27 | 28 | [L3 SVIs: Yellow] [L2 VLANs: Green] [Switches/Routers: Blue] 29 |
30 |
31 | 32 | ## Features 33 | 34 | * Universal Layer2 - Layer4 pathfinding between any two network devices (Full L2 path completion requires NetDB) 35 | * Path Queries can return a single path, or all ECMPs 36 | * L3 Network Database of all networks (Automated, VRF aware, and searchable) 37 | * Search for networks via CIDR or VRF/Role based filters (eg. perim:printers|thinclient, all printers and thin clients in the perim VRF) 38 | * VLAN Inventory of all VLAN instances across the network, segmented by switch domain 39 | * Maps L2 VLAN bridges across switch domains, and calculates local/global STP roots 40 | * Maps L2 paths between devices (regexs supported, eg. dc.* -> dc.* for all datacenter links) 41 | * Reports the configured VLANs and actual VLANs existing on each link for all L2 paths 42 | * Secure REST API Server and Client 43 | * High performance, low latency queries (All queries are sub-second) 44 | * Easily extendable to support mixed-vendor environnments 45 | * Ansible playbooks for a five minute install on Ubuntu 14.04/16.04 46 | 47 | ## Planned Features 48 | * Rewrite API to be more consistent 49 | * Develop a Web frontend 50 | * Integrate with Napalm for datasources 51 | 52 | ## Requirements 53 | 54 | * Python 3.4+ (recommend running via virtualenv) 55 | * Ubuntu or MacOS (should run on any Python compatible platform, but I only support these) 56 | * [Neo4j Graph Database](https://neo4j.com) and Java8 57 | * For Cisco devices, must provide stored configurations (See [Rancid](http://www.shrubbery.net/rancid/) / [Oxidized](https://github.com/ytti/oxidized)) 58 | * Requires CDP/LLDP Discovery Data via [NetDB](http://netdbtracking.sourceforge.net) or [NetCrawl](https://github.com/ytti/netcrawl) 59 | * Third-party network devices need to be parsed into the [NetGrph CSV format](test/csv/) 60 | * Please send me any parsers you create 61 | 62 | ## Supported Devices 63 | 64 | NetGrph is designed to support all network vendors, but currently only has 65 | scrapers available for Cisco IOS and NXOS devices. Adding support for other 66 | devices should be relatively trivial. 67 | 68 | ## About NetGrph 69 | 70 | NetGrph can perform universal L2/L3/L4 path traversals, providing context for 71 | each layer along the path. It also serves as a VLAN and CIDR database, showing 72 | how everything is related. It scales well on even the largest networks, 73 | allowing sub-second queries across thousands of network devices. This enables 74 | the mapping of complex network relationships for discovery and automation. 75 | 76 | Data from queries can be returned as CSV, JSON, YAML, or Ascii tree-art. Network 77 | Visualizations can be created by querying the Neo4j webapp as shown below. The 78 | data model should translate for use with tools such as D3.js, vis.js or Graphwiz 79 | via both the native Neo4j API as well as NetGrph's tree data structure. 80 | 81 | All data is accessible via an API, and the lightweight netgrph client can be 82 | distributed to multiple machines. 83 | 84 | Find out more [About NetGrph](About.md) 85 | 86 | # Documentation Guide 87 | 88 | ### Installation 89 | 90 | See the [Installation Guide](INSTALL.md) 91 | 92 | Ansible Instructions are located [Here](playbooks/README.md) 93 | 94 | ### Tutorials 95 | 96 | See the [Tutorials Documentation](Tutorials.md) 97 | 98 | ### Command Line Interface 99 | 100 | See the [Command Line Interface Documentation](CLI.md) 101 | 102 | ### REST API Documentation 103 | 104 | See the [REST API Documentation](API.md) 105 | 106 | -------------------------------------------------------------------------------- /docs/netgrph-logrotate: -------------------------------------------------------------------------------- 1 | /var/log/nglib.log { 2 | daily 3 | missingok 4 | rotate 7 5 | notifempty 6 | create 666 netdb www-data 7 | } 8 | -------------------------------------------------------------------------------- /docs/netgrph.ini: -------------------------------------------------------------------------------- 1 | # NetGraph INI File 2 | 3 | [Global] 4 | enabled = 1 5 | 6 | [nglib] 7 | dbuser = neo4j 8 | dbpass = your_passwd 9 | dbhost = localhost 10 | 11 | # Make sure this is writable by your user 12 | logfile = nglib.log 13 | 14 | # Default VLAN Range 15 | vrange = 1-1999 16 | 17 | # debuglib, infolib, info, warning, critical 18 | loglevel = info 19 | #loglevel = debuglib 20 | 21 | # Command to run to search FW Logs 22 | logcmd = splunksearch -m 15 -fs -cs 23 | 24 | # URL to provide for log search (primarily splunk for now) 25 | logurl = https://splunk.yourdomain.com/en-US/app/search/search?q=search%%20 26 | 27 | # Optional NetDB Credentials 28 | [netdb] 29 | #host = localhost 30 | #user = netdbadmin 31 | #pass = netdbpass 32 | 33 | [topology] 34 | max_distance = 100 35 | seeds = core1,core2 36 | 37 | # Prioritize certain switches in NEI_EQ decisions, name usually takes precedence 38 | nei_priority = switch2,switch1 39 | 40 | # Exclude certain switches from distance calculations (py regex) 41 | #dist_exclude = (voip|bldg10\-mwavesw1) 42 | dist_exclude = (noexclusion) 43 | 44 | # Rewrite the default VRF on these devices 45 | [default_vrf] 46 | #dc-perim = perim 47 | 48 | # API Client Config 49 | #[api] 50 | #user = testuser 51 | #pass = testpass 52 | #url = http://localhost:4096/netgrph/api/v1.0/ 53 | #verify = 1 54 | 55 | # API Server Configuration 56 | [apisrv] 57 | app_name = netgrph 58 | port = 4096 59 | 60 | # Debug mode logs to stdout and enable Flask debugging 61 | # Set to 0 for production! 62 | debug = 0 63 | logfile = api.log 64 | database = ../api.db 65 | 66 | # SSL Certificate and Key, required for enabling HTTPS 67 | # HTTPS must not be enabled to listen on 0.0.0.0 68 | https = 0 69 | ssl_crt = ssl.crt 70 | ssl_key = ssl.key 71 | 72 | [ngfiles] 73 | vrfs = ./test/csv/vrfs.csv 74 | devices = ./test/csv/devices.csv 75 | device_info = ./test/csv/devinfo.csv 76 | neighbors = ./test/csv/nd.csv 77 | networks = ./test/csv/allnets.csv 78 | vlans = ./test/csv/allvlans.csv 79 | supernets = ./test/csv/supernets.csv 80 | firewalls = ./test/csv/firewalls.csv 81 | links = ./test/csv/links.csv 82 | 83 | # ASA FW Directory 84 | [ngfw] 85 | fwdir = /tftpboot/asafw/ 86 | 87 | 88 | ## NetAlert Section 89 | 90 | [NetAlert] 91 | from = from@testnet.com 92 | subject = New Network Alert 93 | mailServer = yourmailhost.com 94 | 95 | [NetAlertGroups] 96 | test_group = emailfor@group.com 97 | 98 | 99 | # Filters Networks based on vrf:role (from supernets) 100 | # Examples: 101 | # net = all (all networks) 102 | # internal_access = default:none|access-private|access-wan (default VRF only with no supernet role or roles in the new access layer supernets 10.177 etc) 103 | # security = default:none|access-private|access-wan pci:all (security wants all access layer networks plus all PCI networks) 104 | # fwutil = fwutil:all (Wants all fwutil networks) 105 | # com = default:printer (COM wants only printer networks) 106 | 107 | [NetAlertFilter] 108 | test_group = all 109 | -------------------------------------------------------------------------------- /docs/playbooks/README.md: -------------------------------------------------------------------------------- 1 | ## Ansible NetGrph Playbooks 2 | 3 | These playbooks will install NetGrph via Ansible on Ubuntu 14.04 or 16.04 for 4 | you. It will not set your database password or configure your netgrph.ini file 5 | for now, so you need to do that manually. Once the scripts run, browse to 6 | http://machine:7474 and setup your password. Set that same password in 7 | /home/netgrph/docs/netgrph.ini, and run the /home/netgrph/test/first_import.sh. 8 | 9 | #### Setting up Ansible to run via localhost 10 | 11 | ``` 12 | sudo su - 13 | apt-get install ansible 14 | echo '[netgrph]' >> /etc/ansible/hosts 15 | echo localhost ansible_connection=local >> /etc/ansible/hosts 16 | exit 17 | ``` 18 | 19 | #### Run playbooks against localhost (installs under netgrph user) 20 | 21 | ``` 22 | git clone https://github.com/yantisj/netgrph.git /tmp/netgrph/ 23 | cd /tmp/netgrph/docs/playbooks/ 24 | ansible-playbook netgrph.yml --ask-sudo-pass 25 | ``` 26 | 27 | #### Test the install 28 | 29 | ``` 30 | sudo su - netgrph 31 | cd netgrph 32 | ``` 33 | 34 | ##### Use an insecure DB password for testing (not recommended) 35 | ``` 36 | ./test/set_neo4j_password.sh 37 | ``` 38 | 39 | ##### Test a database import 40 | ``` 41 | ./test/first_import.sh 42 | ``` 43 | 44 | - See the INSTALL.md file for test queries and production install information 45 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph.yml: -------------------------------------------------------------------------------- 1 | # Configure your nguser account under vars: below 2 | # Add your server to [netgrph] in Ansible Hosts 3 | # Add your sudo user to [netgrp:vars] 4 | # ansible_ssh_user = yantisj 5 | 6 | - hosts: netgrph 7 | become: yes 8 | become_user: root 9 | vars: 10 | nguser: netgrph 11 | option_install_java: false 12 | option_allow_remote_connections: true 13 | neo4j_edition: neo4j 14 | 15 | neo4j_properties: 16 | - {regexp: "^allow_store_upgrade.*", line: "allow_store_upgrade=true"} 17 | 18 | tasks: 19 | - name: Update Apt Packages 20 | include: tasks/apt-update.yml 21 | 22 | - name: Apt Dependencies 23 | include: tasks/apt-packages.yml 24 | 25 | - name: Install Java8 26 | include: tasks/java8.yml 27 | 28 | - name: Install Neo4j 29 | include: tasks/neo4j.yml 30 | 31 | - name: Add netgrph user 32 | include: tasks/netgrph-user.yml 33 | 34 | - name: Clone NetGrph Repo 35 | include: tasks/clonerepo.yml 36 | 37 | - name: Install PIP Requirements 38 | include: tasks/ngpip.yml 39 | 40 | - name: NetGrph Setup 41 | include: tasks/setup.yml 42 | 43 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/netgrph.yml: -------------------------------------------------------------------------------- 1 | # Configure your nguser account under vars: below 2 | # Add your server to [netgrph] in Ansible Hosts 3 | # Add your sudo user to [netgrp:vars] 4 | # ansible_ssh_user = yantisj 5 | 6 | - hosts: netgrph 7 | become: yes 8 | become_user: root 9 | vars: 10 | nguser: yantisj 11 | option_install_java: false 12 | option_allow_remote_connections: true 13 | neo4j_edition: neo4j 14 | 15 | neo4j_properties: 16 | - {regexp: "^allow_store_upgrade.*", line: "allow_store_upgrade=true"} 17 | 18 | tasks: 19 | - name: Update Apt Packages 20 | include: tasks/apt-update.yml 21 | 22 | - name: Apt Dependencies 23 | include: tasks/apt-packages.yml 24 | 25 | - name: Install Java8 26 | include: tasks/java8.yml 27 | 28 | - name: Install Neo4j 29 | include: tasks/neo4j.yml 30 | 31 | - name: Clone NetGrph Repo 32 | become: yes 33 | become_user: "{{nguser}}" 34 | include: tasks/clonerepo.yml 35 | 36 | - name: Install PIP Requirements 37 | include: tasks/ngpip.yml 38 | 39 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/apt-packages.yml: -------------------------------------------------------------------------------- 1 | # file: update.yml 2 | - name: Install Apt Packages 3 | apt: name={{item}} state=present 4 | with_items: 5 | - python3-pip 6 | - python3-pytest 7 | - python3-logilab-common 8 | - emacs24-nox 9 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/apt-update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update Packages 3 | apt: update_cache=yes upgrade=dist dpkg_options='force-confold,force-confdef' 4 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/clonerepo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Clone NetGrph Repo 3 | git: repo=https://github.com/yantisj/netgrph dest=/home/{{nguser}}/netgrph 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/java8.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: add repo for java 8 3 | apt_repository: repo='ppa:webupd8team/java' state=present 4 | 5 | - name: set licence selected 6 | shell: /bin/echo debconf shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections 7 | sudo: yes 8 | 9 | - name: set licence seen 10 | shell: /bin/echo debconf shared/accepted-oracle-license-v1-1 seen true | /usr/bin/debconf-set-selections 11 | sudo: yes 12 | 13 | - name: install java 8 14 | apt: name=oracle-java8-installer state=latest update-cache=yes force=yes 15 | sudo: yes 16 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/neo4j.yml: -------------------------------------------------------------------------------- 1 | # According to http://debian.neo4j.org/ 2 | --- 3 | - name: NEO4J | Add key for Neo4j repo 4 | apt_key: url=http://debian.neo4j.org/neotechnology.gpg.key state=present 5 | become: yes 6 | 7 | - name: NEO4J | Add Neo4j repo to sources list 8 | apt_repository: repo='deb http://debian.neo4j.org/repo stable/' state=present 9 | become: yes 10 | 11 | - name: NEO4J | Install Neo4j packages 12 | apt: pkg={{ item }} state=installed update_cache=yes force=yes 13 | become: yes 14 | with_items: 15 | - "{{neo4j_edition}}" 16 | 17 | # http://www.delimited.io/blog/2014/1/15/getting-started-with-neo4j-on-ubuntu-server 18 | 19 | - name: NEO4J | Update /etc/security/limits.conf file (1/2) 20 | lineinfile: dest=/etc/security/limits.conf 21 | insertbefore='# End of file' 22 | line='neo4j soft nofile 40000' 23 | state=present 24 | become: yes 25 | 26 | - name: NEO4J | Update /etc/security/limits.conf file (2/2) 27 | lineinfile: dest=/etc/security/limits.conf 28 | insertbefore='# End of file' 29 | line='neo4j hard nofile 40000' 30 | state=present 31 | become: yes 32 | 33 | - name: NEO4J | Update /etc/pam.d/su file 34 | lineinfile: dest=/etc/pam.d/su 35 | regexp="^session required pam_limits.so" 36 | insertafter='^# session required pam_limits.so' 37 | line="session required pam_limits.so" 38 | state=present 39 | become: yes 40 | 41 | # - name: NEO4J | Update /etc/neo4j/neo4j-server.properties to enable remote users to login to neo4j 42 | # lineinfile: dest=/etc/neo4j/neo4j-server.properties 43 | # regexp="^org.neo4j.server.webserver.address=0.0.0.0" 44 | # insertafter='^#org.neo4j.server.webserver.address=0.0.0.0' 45 | # line="org.neo4j.server.webserver.address=0.0.0.0" 46 | # state=present 47 | 48 | - name: NEO4J | Update /etc/neo4j/neo4j.conf to enable remote users to login to neo4j 49 | lineinfile: dest=/etc/neo4j/neo4j.conf 50 | regexp="^dbms.connector.http.address=0.0.0.0:7474" 51 | insertafter='^#dbms.connector.http.address=0.0.0.0:7474' 52 | line="dbms.connector.http.address=0.0.0.0:7474" 53 | state=present 54 | when: option_allow_remote_connections 55 | become: yes 56 | 57 | - name: NEO4J | Restart Neo4j 58 | service: name=neo4j state=restarted 59 | become: yes 60 | -------------------------------------------------------------------------------- /docs/playbooks/netgrph/tasks/ngpip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install PIP Requirements via PIP3 3 | pip: requirements=/home/{{nguser}}/netgrph/requirements.txt executable=pip3 4 | 5 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/apt-packages.yml: -------------------------------------------------------------------------------- 1 | # file: update.yml 2 | - name: APT | Install Apt Packages 3 | apt: name={{item}} state=present 4 | with_items: 5 | - python3-pip 6 | - python3-pytest 7 | - python3-logilab-common 8 | - emacs24-nox 9 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/apt-update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: APT | Update Packages 3 | apt: update_cache=yes upgrade=dist dpkg_options='force-confold,force-confdef' 4 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/clonerepo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: GIT | Clone NetGrph Repo 3 | git: repo=https://github.com/yantisj/netgrph dest=/home/{{nguser}}/netgrph 4 | become: yes 5 | become_user: "{{nguser}}" 6 | 7 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/java8.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: JAVA | Add repo for java 8 3 | apt_repository: repo='ppa:webupd8team/java' mode=664 state=present 4 | 5 | - name: JAVA | Set licence selected 6 | shell: /bin/echo debconf shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections 7 | 8 | - name: JAVA | Set licence seen 9 | shell: /bin/echo debconf shared/accepted-oracle-license-v1-1 seen true | /usr/bin/debconf-set-selections 10 | 11 | - name: JAVA | Install java 8 12 | apt: name=oracle-java8-installer state=latest update-cache=yes force=yes 13 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/neo4j.yml: -------------------------------------------------------------------------------- 1 | # According to http://debian.neo4j.org/ 2 | --- 3 | - name: NEO4J | Add key for Neo4j repo 4 | apt_key: url=http://debian.neo4j.org/neotechnology.gpg.key state=present 5 | become: yes 6 | 7 | - name: NEO4J | Add Neo4j repo to sources list 8 | apt_repository: repo='deb http://debian.neo4j.org/repo stable/' mode=664 state=present 9 | become: yes 10 | 11 | - name: NEO4J | Install Neo4j packages 12 | apt: pkg={{ item }} state=installed update_cache=yes force=yes 13 | become: yes 14 | with_items: 15 | - "{{neo4j_edition}}" 16 | 17 | # http://www.delimited.io/blog/2014/1/15/getting-started-with-neo4j-on-ubuntu-server 18 | 19 | - name: NEO4J | Update /etc/security/limits.conf file (1/2) 20 | lineinfile: dest=/etc/security/limits.conf 21 | insertbefore='# End of file' 22 | line='neo4j soft nofile 40000' 23 | state=present 24 | become: yes 25 | 26 | - name: NEO4J | Update /etc/security/limits.conf file (2/2) 27 | lineinfile: dest=/etc/security/limits.conf 28 | insertbefore='# End of file' 29 | line='neo4j hard nofile 40000' 30 | state=present 31 | become: yes 32 | 33 | - name: NEO4J | Update /etc/pam.d/su file 34 | lineinfile: dest=/etc/pam.d/su 35 | regexp="^session required pam_limits.so" 36 | insertafter='^# session required pam_limits.so' 37 | line="session required pam_limits.so" 38 | state=present 39 | become: yes 40 | 41 | # - name: NEO4J | Update /etc/neo4j/neo4j-server.properties to enable remote users to login to neo4j 42 | # lineinfile: dest=/etc/neo4j/neo4j-server.properties 43 | # regexp="^org.neo4j.server.webserver.address=0.0.0.0" 44 | # insertafter='^#org.neo4j.server.webserver.address=0.0.0.0' 45 | # line="org.neo4j.server.webserver.address=0.0.0.0" 46 | # state=present 47 | 48 | - name: NEO4J | Update /etc/neo4j/neo4j.conf to enable remote users to login to neo4j 49 | lineinfile: dest=/etc/neo4j/neo4j.conf 50 | regexp="^dbms.connector.http.address=0.0.0.0:7474" 51 | insertafter='^#dbms.connector.http.address=0.0.0.0:7474' 52 | line="dbms.connector.http.address=0.0.0.0:7474" 53 | state=present 54 | when: option_allow_remote_connections 55 | become: yes 56 | 57 | - name: NEO4J | Restart Neo4j 58 | service: name=neo4j state=restarted 59 | become: yes 60 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/netgrph-user.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - group: name=netgrph state=present 3 | - user: name=netgrph comment="NetGrph User" group=netgrph generate_ssh_key=yes ssh_key_bits=2048 ssh_key_file=.ssh/id_rsa 4 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/ngpip.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: PIP | Install NetGrph Requirements via PIP3 3 | pip: requirements=/home/{{nguser}}/netgrph/requirements.txt executable=pip3 4 | 5 | -------------------------------------------------------------------------------- /docs/playbooks/tasks/setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: NetGrph | Symlinking executables 4 | file: src=/home/{{ nguser }}/netgrph/netgrph.py dest=/usr/local/bin/netgrph state=link 5 | - file: src=/home/{{ nguser }}/netgrph/ngupdate.py dest=/usr/local/bin/ngupdate state=link 6 | - file: src=/home/{{ nguser }}/netgrph/ngreport.py dest=/usr/local/bin/ngreport state=link 7 | 8 | -------------------------------------------------------------------------------- /docs/playbooks/test.yml: -------------------------------------------------------------------------------- 1 | # Configure your nguser account under vars: below 2 | # Add your server to [netgrph] in Ansible Hosts 3 | # Add your sudo user to [netgrp:vars] 4 | # ansible_ssh_user = yantisj 5 | 6 | - hosts: netgrph 7 | become: yes 8 | become_user: root 9 | vars: 10 | nguser: netgrph 11 | option_install_java: false 12 | option_allow_remote_connections: true 13 | neo4j_edition: neo4j 14 | 15 | neo4j_properties: 16 | - {regexp: "^allow_store_upgrade.*", line: "allow_store_upgrade=true"} 17 | 18 | tasks: 19 | # - name: Update Apt Packages 20 | # include: tasks/apt-update.yml 21 | 22 | # - name: Apt Dependencies 23 | # include: tasks/apt-packages.yml 24 | 25 | # - name: Install Java8 26 | # include: tasks/java8.yml 27 | 28 | # - name: Install Neo4j 29 | # include: tasks/neo4j.yml 30 | 31 | # - name: Add netgrph user 32 | # include: tasks/netgrph-user.yml 33 | 34 | # - name: Clone NetGrph Repo 35 | # become: yes 36 | # become_user: "{{nguser}}" 37 | # include: tasks/clonerepo.yml 38 | 39 | # - name: Install PIP Requirements 40 | # include: tasks/ngpip.yml 41 | 42 | - name: NetGrph Setup 43 | include: tasks/setup.yml 44 | -------------------------------------------------------------------------------- /docs/workflow.txt: -------------------------------------------------------------------------------- 1 | 2 | edit docs/VERSION 3 | git tag v0.7.x 4 | git merge --no-ff --log origin/dev 5 | git push origin master 6 | 7 | -------------------------------------------------------------------------------- /extra/apisrv-nginx: -------------------------------------------------------------------------------- 1 | server { 2 | listen 9000 ssl; 3 | server_name 0.0.0.0; 4 | ssl_certificate /home/netgrph/netgrph/newhal.crt; 5 | ssl_certificate_key /home/netgrph/netgrph/newhal.key; 6 | 7 | location / { 8 | include uwsgi_params; 9 | uwsgi_pass unix:/home/netgrph/netgrph/apisrv.sock; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /extra/nsj-sample.ini: -------------------------------------------------------------------------------- 1 | # NSJ Config 2 | [nsj] 3 | username = switch-user 4 | password = password 5 | runlog = nsj.log 6 | outlog = job.log 7 | loglevel = warning 8 | threads = 50 9 | debug = 0 10 | 11 | # API Client Config 12 | [api] 13 | user = testuser 14 | pass = testpass 15 | url = http://localhost:4096/netgrph/api/v1.1/ 16 | verify = 0 17 | -------------------------------------------------------------------------------- /extra/wsgi-api.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = wsgi-api 3 | 4 | master = true 5 | processes = 5 6 | 7 | socket = apisrv.sock 8 | chmod-socket = 666 9 | vacuum = true 10 | 11 | die-on-term = true 12 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: NetGrph 2 | pages: 3 | - Home: index.md 4 | - About NetGrph: About.md 5 | - Installing NetGrph: 6 | - Installation: INSTALL.md 7 | - Ansible Install: playbooks/README.md 8 | - Using NetGrph: 9 | - Usage Overview: Tutorials.md 10 | - Command Line Interface: CLI.md 11 | - Topology and Pathfinding: PathSample.md 12 | - REST API Reference: API.md 13 | -------------------------------------------------------------------------------- /nglib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # 4 | # Copyright (c) 2016 "Jonathan Yantis" 5 | # 6 | # This file is a part of NetGrph. 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License, version 3, 10 | # as published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | # 20 | # As a special exception, the copyright holders give permission to link the 21 | # code of portions of this program with the OpenSSL library under certain 22 | # conditions as described in each individual source file and distribute 23 | # linked combinations including the program with the OpenSSL library. You 24 | # must comply with the GNU Affero General Public License in all respects 25 | # for all of the code used other than as permitted herein. If you modify 26 | # file(s) with this exception, you may extend this exception to your 27 | # version of the file(s), but you are not obligated to do so. If you do not 28 | # wish to do so, delete this exception statement from your version. If you 29 | # delete this exception statement from all source files in the program, 30 | # then also delete it in the license file. 31 | # 32 | # 33 | """ 34 | NGDB Main Library 35 | - Main Library functions and variables used by modules 36 | - Call module.init_nglib(config) to initialize 37 | - Sets logging levels, loads config file 38 | """ 39 | import csv 40 | import re 41 | import sys 42 | import os 43 | import pwd 44 | import datetime 45 | import configparser 46 | import logging 47 | 48 | try: 49 | from neo4j.v1 import TRUST_ON_FIRST_USE, TRUST_SIGNED_CERTIFICATES, SSL_AVAILABLE 50 | from neo4j.v1.exceptions import CypherError, ProtocolError 51 | from neo4j.v1 import GraphDatabase, basic_auth 52 | from py2neo import Node, Relationship, Graph 53 | except ImportError: 54 | pass 55 | 56 | logger = logging.getLogger(__name__) 57 | 58 | # Global variables (all library global variables go here) 59 | verbose = 0 60 | config = None 61 | 62 | # Save user for library 63 | user = pwd.getpwuid(os.getuid())[0] 64 | 65 | # DB Sessions accessed globally 66 | bolt_ses = None 67 | py2neo_ses = None 68 | 69 | # Topology Variables 70 | max_distance = 100 71 | dev_seeds = None 72 | 73 | # NetDB Enabled 74 | use_netdb = False 75 | 76 | def get_bolt_db(): 77 | """Return Bolt Session""" 78 | 79 | # DB Credentials 80 | dbuser = config['nglib']['dbuser'] 81 | dbpass = config['nglib']['dbpass'] 82 | dbhost = config['nglib']['dbhost'] 83 | 84 | return get_db_client(dbhost, dbuser, dbpass, bolt=True) 85 | 86 | def get_py2neo_db(): 87 | """Return Bolt Session""" 88 | 89 | # DB Credentials 90 | dbuser = config['nglib']['dbuser'] 91 | dbpass = config['nglib']['dbpass'] 92 | dbhost = config['nglib']['dbhost'] 93 | 94 | return get_db_client(dbhost, dbuser, dbpass, bolt=False) 95 | 96 | def get_db_client(dbhost, dbuser, dbpass, bolt=False): 97 | """Return a Neo4j DB session. bolt=True uses bolt driver""" 98 | 99 | if verbose > 4: 100 | print("DB Creds", dbhost, dbuser, dbpass) 101 | 102 | if bolt: 103 | bolt_url = "bolt://" + dbhost 104 | auth_token = basic_auth(dbuser, dbpass) 105 | try: 106 | driver = GraphDatabase.driver(bolt_url, auth=auth_token, max_pool_size=5) 107 | bolt_session = driver.session() 108 | return bolt_session 109 | except Exception as e: 110 | print("Database connection/authentication error:", e) 111 | sys.exit(1) 112 | else: 113 | login = "http://{0}:{1}@{2}:7474/db/data/".format(dbuser, dbpass, dbhost) 114 | py2neo_session = Graph(login) 115 | return py2neo_session 116 | 117 | def drop_database(): 118 | """Drop all database data (requires a full import to restore)""" 119 | 120 | logger.warning('Dropping Database') 121 | 122 | bolt_ses.run("MATCH ()-[e]-() DELETE e") 123 | bolt_ses.run("MATCH (n) DELETE n") 124 | 125 | 126 | def import_cypher(fileName): 127 | """Import MATCH/CREATE/MERGE Cypher Commands Directly""" 128 | f = open(fileName) 129 | 130 | for line in f: 131 | if re.search(r'^(MATCH|CREATE|MERGE)', line): 132 | line = line.strip() 133 | logger.debug(line) 134 | results = bolt_ses.run(line) 135 | 136 | test = results.consume() 137 | if test: 138 | logger.info('Executed ' + test.statement) 139 | 140 | def get_time(hours=None): 141 | """Get current time, optionally time shifted by hours""" 142 | 143 | if hours: 144 | timeShifted = datetime.datetime.now() - datetime.timedelta(hours=hours) 145 | return str(timeShifted) 146 | 147 | time = str(datetime.datetime.now()) 148 | return time 149 | 150 | 151 | def getEntry(l, pos=0): 152 | """Returns first entry in a list, or at position pos=x""" 153 | 154 | return l[pos] 155 | 156 | 157 | def importCSVasDict(fileName): 158 | """Imports a file with DictReader""" 159 | 160 | f = open(fileName) 161 | db = csv.DictReader(f) 162 | return db 163 | 164 | 165 | def importCSVasList(fileName): 166 | """Imports CSV File as a list""" 167 | 168 | f = open(fileName) 169 | flist = csv.DictReader(f) 170 | 171 | return flist 172 | 173 | 174 | # Initialize Configuration 175 | def init_nglib(configFile, initdb=True): 176 | """Initializes Library based on config file 177 | 178 | - Sets global variables in library for use with other modules 179 | - Configures debugging and sets up logging 180 | - Modifies py2neo and bolt library levels 181 | 182 | """ 183 | 184 | # Global variables for use throughout library 185 | global config 186 | global max_distance 187 | global dev_seeds 188 | global bolt_ses 189 | global py2neo_ses 190 | global use_netdb 191 | 192 | if verbose > 1: 193 | print("Config File", configFile) 194 | 195 | config = configparser.ConfigParser() 196 | config.read(configFile) 197 | 198 | # Initialize Logging 199 | init_logging() 200 | 201 | # Tries to Loads NetDB Variables 202 | try: 203 | use_netdb = config['netdb']['host'] 204 | except KeyError: 205 | pass 206 | if use_netdb: 207 | use_netdb = True 208 | 209 | if initdb: 210 | # DB Credentials 211 | dbuser = config['nglib']['dbuser'] 212 | dbpass = config['nglib']['dbpass'] 213 | dbhost = config['nglib']['dbhost'] 214 | 215 | # Login to DB for parent Variables 216 | if verbose > 1: 217 | logger.info("Connecting to Neo4j: %s %s", dbhost, dbuser) 218 | bolt_ses = get_db_client(dbhost, dbuser, dbpass, bolt=True) 219 | py2neo_ses = get_db_client(dbhost, dbuser, dbpass) 220 | 221 | # Topology 222 | max_distance = int(config['topology']['max_distance']) 223 | dev_seeds = config['topology']['seeds'] 224 | 225 | logger.debug("Initialized Configuration Successfully") 226 | 227 | 228 | def init_logging(): 229 | '''Initialize Logging''' 230 | # Default loglevel 231 | level = logging.INFO 232 | 233 | logfile = config['nglib']['logfile'] 234 | loglevel = config['nglib']['loglevel'] 235 | 236 | if loglevel == 'info': 237 | level = logging.INFO 238 | if loglevel == 'debug': 239 | level = logging.DEBUG 240 | if loglevel == 'debuglib': 241 | level = logging.DEBUG 242 | if loglevel == 'warning': 243 | level = logging.WARNING 244 | if loglevel == 'critical': 245 | level = logging.CRITICAL 246 | 247 | # Debug logging 248 | if verbose > 2: 249 | loglevel = 'debug' 250 | level = logging.DEBUG 251 | elif verbose > 1: 252 | loglevel = 'debuglib' 253 | level = logging.DEBUG 254 | 255 | # Verbose Logs to stdout 256 | if verbose: 257 | logging.basicConfig(format='%(asctime)s %(name)s:%(levelname)s: %(message)s', 258 | stream=sys.stdout, level=level) 259 | logger.info("Enabled STDOUT Logging") 260 | 261 | # Log to File 262 | else: 263 | logging.basicConfig(format='%(asctime)s %(name)s:%(levelname)s: %(message)s', 264 | filename=logfile, level=level) 265 | 266 | # Silence Cypher Logging 267 | if loglevel == 'info' or loglevel == 'debuglib': 268 | logging.getLogger("py2neo.cypher").setLevel(logging.WARNING) 269 | logging.getLogger("py2neo.batch").setLevel(logging.WARNING) 270 | logging.getLogger("httpstream").setLevel(logging.WARNING) 271 | logging.getLogger("neo4j.bolt").setLevel(logging.WARNING) 272 | -------------------------------------------------------------------------------- /nglib/alerts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | 31 | """Process Alerts for different network events""" 32 | 33 | import configparser 34 | import smtplib 35 | import logging 36 | from email.mime.text import MIMEText 37 | #from email.mime.multipart import MIMEMultipart 38 | import nglib 39 | 40 | verbose = 0 41 | logger = logging.getLogger(__name__) 42 | 43 | # Generate Alerts on new networks 44 | def gen_new_network_alerts(): 45 | """Generate New Network Alerts""" 46 | 47 | gAlert = dict() 48 | newNets = [] 49 | 50 | # Find any new networks 51 | results = nglib.py2neo_ses.cypher.execute( 52 | 'MATCH(n:NewNetwork) return n.vrfcidr AS vrfcidr') 53 | 54 | ncount = len(results) 55 | 56 | if ncount > 0: 57 | logger.info("Found %s New Networks to Alert On", ncount) 58 | 59 | # Load groups in to dictionary to add list of networks to 60 | loadGroups(gAlert) 61 | 62 | # Append all networks to a list 63 | for network in results: 64 | #print(network.vrfcidr) 65 | newNets.append(network.vrfcidr) 66 | 67 | # Filter Nets for each group and add to their list to alert 68 | loadNetAlerts(gAlert, newNets) 69 | 70 | # Send Alerts to each group 71 | for g in gAlert.keys(): 72 | if verbose > 1: 73 | print("\n\nALERTS for {0}:\n".format(g)) 74 | acount = len(gAlert[g]) 75 | logger.info("Sending %s New Network Alerts to %s", acount, g) 76 | 77 | if not verbose: 78 | sendEmailAlert(g, gAlert[g]) 79 | 80 | # Deleting NewNetwork Objects 81 | if not verbose: 82 | results = nglib.py2neo_ses.cypher.execute( 83 | 'MATCH(n:NewNetwork) delete n') 84 | 85 | def gen_new_vlan_alerts(): 86 | """ Send new VLAN Alerts to all groups that receive 'all' alerts """ 87 | 88 | newVlans = [] 89 | groups = [] 90 | 91 | # Find any new networks 92 | results = nglib.py2neo_ses.cypher.execute( 93 | 'MATCH(v:NewVLAN) return v.name AS name') 94 | 95 | ncount = len(results) 96 | 97 | if ncount > 0: 98 | logger.info("Found %s New Vlans to Alert On", ncount) 99 | 100 | # 101 | for group in nglib.config['NetAlertGroups']: 102 | if nglib.config['NetAlertFilter'][group] == 'all': 103 | groups.append(group) 104 | 105 | # Append all vlans to a list 106 | for vlan in results: 107 | newVlans.append(vlan.name) 108 | 109 | if nglib.verbose: 110 | print(groups) 111 | print(newVlans) 112 | 113 | for group in groups: 114 | sendEmailAlert(group, newVlans, vlan=True) 115 | 116 | # Deleting NewVLAN Objects 117 | if not verbose: 118 | results = nglib.py2neo_ses.cypher.execute( 119 | 'MATCH(n:NewVLAN) delete n') 120 | 121 | def loadGroups(gAlert): 122 | """Load all groups from config file into a dictionary of lists""" 123 | 124 | for group in nglib.config['NetAlertGroups']: 125 | gAlert[group] = [] 126 | 127 | 128 | def loadNetAlerts(gAlert, newNets): 129 | """Add networks to each group after filtering attributes""" 130 | 131 | for net in newNets: 132 | 133 | netDict = nglib.query.net.get_net_props(net) 134 | 135 | if len(netDict.keys()): 136 | for group in gAlert.keys(): 137 | if nglib.query.check_net_filter(netDict, group): 138 | logger.debug("Adding " + netDict['CIDR'] + " to alerts for " + group) 139 | gAlert[group].append(netDict) 140 | 141 | 142 | def sendEmailAlert(group, nList, vlan=False): 143 | """Send group Network Alert from matched list""" 144 | 145 | if nglib.verbose: 146 | print("Emailing {0} New Network List Alert:\n {1}".format(group, str(nList))) 147 | 148 | emailAddress = nglib.config['NetAlertGroups'][group] 149 | fromAddress = nglib.config['NetAlert']['from'] 150 | mailServer = nglib.config['NetAlert']['mailServer'] 151 | subject = nglib.config['NetAlert']['subject'] 152 | 153 | if vlan: 154 | subject = nglib.config['NetAlert']['vlansubject'] 155 | 156 | nTable = "" 157 | 158 | for n in nList: 159 | nTable = nTable + "\n" + str(n) 160 | 161 | nTable = "
".join(nTable.split("\n")) 162 | 163 | contents = '' + nTable + '' 164 | 165 | msg = MIMEText(contents, 'html') 166 | msg['Subject'] = subject 167 | msg['From'] = fromAddress 168 | msg['To'] = emailAddress 169 | 170 | s = smtplib.SMTP(mailServer) 171 | s.sendmail(fromAddress, [emailAddress], msg.as_string()) 172 | s.quit() 173 | 174 | if nglib.verbose: 175 | print(emailAddress, fromAddress, subject, contents) 176 | 177 | # END 178 | -------------------------------------------------------------------------------- /nglib/cache_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | 31 | """NetGrph Cache Management""" 32 | 33 | import logging 34 | from nglib.query.nNode import getRelationship, getLabel, getJSONProperties 35 | import nglib 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | 40 | def clear_edges(hours): 41 | """ 42 | Clear Expired Edges 43 | 44 | Notes: nglib.verbose returns edges to delete but does not delete 45 | """ 46 | 47 | logger.info("Clearing Edges older than " + str(hours) + " hours") 48 | 49 | # Time shifted datetime 50 | age = nglib.get_time(hours=hours) 51 | 52 | edges = nglib.py2neo_ses.cypher.execute( 53 | 'MATCH ()-[e]->() WHERE e.time < {age} RETURN e', 54 | age=age) 55 | 56 | if len(edges) > 0: 57 | for e in edges: 58 | neighbors = getRelationship(e.e) 59 | logger.info("Expired Edge: " + neighbors) 60 | 61 | count = nglib.py2neo_ses.cypher.execute( 62 | 'MATCH ()-[e]->() WHERE e.time < {age} RETURN count(e) as count', 63 | age=age) 64 | 65 | if nglib.verbose: 66 | logger.info("Expired Edges: " + str(count[0].count)) 67 | else: 68 | logger.info("Deleting Edges: " + str(count[0].count)) 69 | nglib.py2neo_ses.cypher.execute( 70 | 'MATCH ()-[e]->() WHERE e.time < {age} DELETE e', 71 | age=age) 72 | 73 | 74 | def clear_nodes(hours): 75 | """ 76 | Clear Expired Nodes 77 | 78 | Notes: verbose returns nodes to delete but does not delete 79 | """ 80 | 81 | logger.info("Finding Nodes to Clear older than " + str(hours) + " hours") 82 | 83 | # Time shifted datetime 84 | age = nglib.get_time(hours=hours) 85 | 86 | nodes = nglib.py2neo_ses.cypher.execute( 87 | 'MATCH (n) WHERE n.time < {age} RETURN n', 88 | age=age) 89 | 90 | if len(nodes) > 0: 91 | 92 | for r in nodes: 93 | label = getLabel(r.n) 94 | pj = getJSONProperties(r.n) 95 | logger.info("Expired Node: " + label + pj['name']) 96 | 97 | count = nglib.py2neo_ses.cypher.execute( 98 | 'MATCH (n) WHERE n.time < {age} RETURN count(n) as count', 99 | age=age) 100 | 101 | logger.info("Expired Nodes: " + str(count[0].count)) 102 | 103 | if not nglib.verbose: 104 | logger.info("Deleting Nodes: " + str(count[0].count)) 105 | 106 | nglib.py2neo_ses.cypher.execute( 107 | 'MATCH (n)-[e]-() WHERE n.time < {age} DELETE e', 108 | age=age) 109 | 110 | nglib.py2neo_ses.cypher.execute( 111 | 'MATCH (n) WHERE n.time < {age} DELETE n', 112 | age=age) 113 | 114 | 115 | def swap_quotes(myString): 116 | """Swap Quote Types for JSON from Neo4j""" 117 | myString = myString.replace("'", '"') 118 | return myString 119 | -------------------------------------------------------------------------------- /nglib/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # 4 | # Copyright (c) 2016 "Jonathan Yantis" 5 | # 6 | # This file is a part of NetGrph. 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License, version 3, 10 | # as published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | # 20 | # As a special exception, the copyright holders give permission to link the 21 | # code of portions of this program with the OpenSSL library under certain 22 | # conditions as described in each individual source file and distribute 23 | # linked combinations including the program with the OpenSSL library. You 24 | # must comply with the GNU Affero General Public License in all respects 25 | # for all of the code used other than as permitted herein. If you modify 26 | # file(s) with this exception, you may extend this exception to your 27 | # version of the file(s), but you are not obligated to do so. If you do not 28 | # wish to do so, delete this exception statement from your version. If you 29 | # delete this exception statement from all source files in the program, 30 | # then also delete it in the license file. 31 | """ 32 | NetGrph Exceptions 33 | """ 34 | 35 | class Error(Exception): 36 | """Base class for exceptions in this module.""" 37 | pass 38 | 39 | class OutputError(Error): 40 | """Exception raised for errors in the output.""" 41 | 42 | def __init__(self, expression, message): 43 | self.expression = expression 44 | self.message = message 45 | 46 | class ResultError(Error): 47 | """Exception raised for Result Errors""" 48 | 49 | def __init__(self, expression, message): 50 | self.expression = expression 51 | self.message = message 52 | -------------------------------------------------------------------------------- /nglib/fw_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """ 32 | NetGrph Firewall Import Routines 33 | """ 34 | import logging 35 | import nglib 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | def import_fw(fileName): 40 | """Import FW CSV File""" 41 | 42 | logger.info("Importing Firewalls from " + fileName) 43 | 44 | fwdb = nglib.importCSVasDict(fileName) 45 | 46 | # Import FW Ints to DB 47 | import_fw_ints(fwdb) 48 | 49 | 50 | def import_fw_ints(fwdb): 51 | """Import Firewall Interfaces from FW File""" 52 | 53 | time = nglib.get_time() 54 | 55 | # Iterate through all firewall ints 56 | for fwint in fwdb: 57 | 58 | # DB Values 59 | name = fwint['Name'] 60 | hostname = fwint['Hostname'] 61 | #IP = fwint['IP'] 62 | vlanInt = fwint['Interface'] 63 | desc = fwint['Description'] 64 | seclevel = fwint['Security-Level'] 65 | logIndex = fwint['Log-Index'] 66 | 67 | #print(name, vlanInt, desc, seclevel, IP, hostname, logIndex) 68 | 69 | vlan = vlanInt.replace('Vlan', '') 70 | 71 | # Search for existing Firewall 72 | results = nglib.py2neo_ses.cypher.execute( 73 | 'MATCH (fw:Switch:Router:FW {name:{name}}) RETURN fw', 74 | name=name) 75 | 76 | # Insert new Firewall 77 | if len(results) == 0: 78 | logger.info("Creating New Firewall: " + name) 79 | 80 | results = nglib.py2neo_ses.cypher.execute( 81 | 'CREATE (fw:Switch:Router:FW {name:{name}, hostname:{hostname}, ' 82 | + 'logIndex:{logIndex}, time:{time}}) RETURN fw', 83 | name=name, hostname=hostname, logIndex=logIndex, time=time) 84 | 85 | # Update FW 86 | else: 87 | logger.debug("Updating Firewall: " + name) 88 | 89 | nglib.py2neo_ses.cypher.execute( 90 | 'MATCH (fw:Switch:Router:FW {name:{name}}) SET fw += ' 91 | + '{hostname:{hostname}, logIndex:{logIndex}, time:{time}} RETURN fw', 92 | name=name, hostname=hostname, logIndex=logIndex, time=time) 93 | 94 | # Search for existing Vlan 95 | results = nglib.py2neo_ses.cypher.execute( 96 | 'MATCH (n:Network {vid:{vlan}})-[e:ROUTED_FW]->' 97 | + '(fw:Switch:Router:FW {name:{name}}) ' 98 | + 'RETURN e', 99 | vlan=vlan, name=name) 100 | 101 | if len(results) == 0: 102 | logger.info("Creating New ROUTED_FW Link: %s --> %s", vlan, name) 103 | 104 | results = nglib.py2neo_ses.cypher.execute( 105 | 'MATCH (n:Network {vid:{vlan}}), (fw:Switch:Router:FW {name:{name}})' 106 | + 'CREATE (n)-[e:ROUTED_FW ' 107 | + '{desc:{desc}, seclevel:{seclevel}, time:{time}}]->(fw)', 108 | vlan=vlan, name=name, desc=desc, seclevel=seclevel, time=time) 109 | 110 | # Update Firewall Interface 111 | else: 112 | logger.debug("Updating ROUTED_FW: %s --> %s", vlan, name) 113 | 114 | results = nglib.py2neo_ses.cypher.execute( 115 | 'MATCH (n:Network {vid:{vlan}})-[e:ROUTED_FW]->' 116 | + '(fw:Switch:Router:FW {name:{name}})' 117 | + 'SET e += {desc:{desc}, seclevel:{seclevel}, time:{time}} ' 118 | + 'RETURN e', 119 | vlan=vlan, name=name, desc=desc, seclevel=seclevel, time=time) 120 | -------------------------------------------------------------------------------- /nglib/netdb/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """ 32 | NetGrph NetDB Interface 33 | """ 34 | 35 | import datetime 36 | import locale 37 | import csv 38 | import re 39 | import os 40 | import sys 41 | import logging 42 | import pymysql 43 | import nglib 44 | 45 | 46 | logger = logging.getLogger(__name__) 47 | 48 | # DB connection is global 49 | netdb_ses = None 50 | 51 | def connect_netdb(): 52 | """Update the netdb_ses handle""" 53 | 54 | global netdb_ses 55 | netdbhost = None 56 | netdbuser = None 57 | netdbpasswd = None 58 | 59 | try: 60 | netdbhost = nglib.config['netdb']['host'] 61 | netdbuser = nglib.config['netdb']['user'] 62 | netdbpasswd = nglib.config['netdb']['pass'] 63 | 64 | except KeyError: 65 | print("Error: NetDB Database Credentials no Configured", file=sys.stderr) 66 | raise 67 | 68 | if not netdbuser: 69 | raise Exception("NetDB Credentials not Configured") 70 | 71 | netdb_ses = pymysql.connect(user=netdbuser, password=netdbpasswd, 72 | host=netdbhost, 73 | database="netdb") 74 | 75 | return netdb_ses 76 | 77 | 78 | def get_lastseen(hours=168): 79 | """Get lastseen string for MySQL""" 80 | 81 | lastseen = datetime.datetime.now() - datetime.timedelta(hours=hours) 82 | lastseen = lastseen.isoformat() 83 | return lastseen 84 | 85 | 86 | def get_mac_and_port_counts(switch, vlan): 87 | """Get the number of mac addresses and ports on a VLAN for a switch""" 88 | 89 | connect_netdb() 90 | 91 | lastseen = get_lastseen() 92 | 93 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 94 | 95 | cursor.execute("SELECT count(vlan) AS pcount FROM switchstatus " 96 | + "WHERE switch = '{}' and vlan='{}'".format(switch, vlan)) 97 | 98 | pc = cursor.fetchall() 99 | pcount = pc[0]['pcount'] 100 | 101 | # MACs on switch with lastseen 102 | mac_query = "SELECT count(mac) AS mcount FROM switchports " 103 | mac_query += "WHERE switch = '{}' AND s_vlan='{}' ".format(switch, vlan) 104 | mac_query += "AND lastseen > '{}'".format(lastseen) 105 | cursor.execute(mac_query) 106 | mc = cursor.fetchall() 107 | mcount = mc[0]['mcount'] 108 | 109 | return(pcount, mcount) 110 | -------------------------------------------------------------------------------- /nglib/netdb/ip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """ 32 | NetGrph NetDB IP Interface 33 | """ 34 | import re 35 | import socket 36 | import logging 37 | import pymysql 38 | import functools 39 | import nglib.netdb 40 | import nglib.ngtree 41 | 42 | 43 | logger = logging.getLogger(__name__) 44 | 45 | 46 | @functools.lru_cache(maxsize=2) 47 | def get_netdb_ip(ip, hours=720): 48 | """Get All Details for an IP""" 49 | 50 | # Check for non-ip, try DNS 51 | if not re.search(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip): 52 | try: 53 | ip = socket.gethostbyname(ip) 54 | except socket.gaierror: 55 | raise Exception("Hostname Lookup Failure on: " + ip) 56 | 57 | netdb_ses = nglib.netdb.connect_netdb() 58 | 59 | lastseen = nglib.netdb.get_lastseen(hours) 60 | 61 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 62 | # cursor = netdb_ses.cursor(dictionary=True) 63 | 64 | cursor.execute("SELECT * FROM superarp " 65 | + "WHERE ip = '{}' AND lastseen > '{}'".format(ip, lastseen)) 66 | 67 | pc = cursor.fetchall() 68 | 69 | multi_entry = False 70 | pngtree = nglib.ngtree.get_ngtree("IPs", tree_type="NetDB") 71 | 72 | if not len(pc): 73 | return None 74 | 75 | # If multiple entries, primary returns under ngtree, the rest return 76 | # as historical nested data 77 | if len(pc) > 1: 78 | multi_entry = True 79 | 80 | latest = None 81 | 82 | # Gather details from DB in ngtree structure 83 | for en in pc: 84 | ngtree = nglib.ngtree.get_ngtree("IP", tree_type="NetDB") 85 | ngtree['Name'] = ip 86 | 87 | ngtree['firstSeen'] = str(en['firstseen']) 88 | ngtree['lastSeen'] = str(en['lastseen']) 89 | ngtree['MAC'] = en['mac'] 90 | ngtree['FQDN'] = en['name'] 91 | ngtree['vendor'] = en['vendor'] 92 | ngtree['Switch'] = en['lastswitch'] 93 | ngtree['SwitchPort'] = en['lastport'] 94 | ngtree['UserID'] = en['userID'] 95 | ngtree['VLAN'] = en['vlan'] 96 | 97 | 98 | if not latest: 99 | latest = ngtree 100 | elif latest['lastSeen'] < ngtree['lastSeen']: 101 | latest = ngtree 102 | 103 | nglib.ngtree.add_child_ngtree(pngtree, ngtree) 104 | 105 | # Return as parent if single entry 106 | if not multi_entry: 107 | return ngtree 108 | 109 | # Return latest IP as root and nest others under a new tree 110 | multitree = nglib.ngtree.get_ngtree("Historical IPs", tree_type="NetDB") 111 | for en in pngtree: 112 | if '_child' in en and pngtree[en] != latest: 113 | nglib.ngtree.add_child_ngtree(multitree, pngtree[en]) 114 | nglib.ngtree.add_child_ngtree(latest, multitree) 115 | return latest 116 | 117 | def arp(router, hours=1): 118 | """Pull the ARP Table on a router from NetDB, use router='%' for everything""" 119 | 120 | netdb_ses = nglib.netdb.connect_netdb() 121 | 122 | lastseen = nglib.netdb.get_lastseen(hours) 123 | 124 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 125 | # cursor = netdb_ses.cursor(dictionary=True) 126 | 127 | cursor.execute("SELECT * FROM superarp " 128 | + "WHERE router LIKE '{}' AND lastseen > '{}'".format(router, lastseen)) 129 | 130 | pc = cursor.fetchall() 131 | 132 | pngtree = nglib.ngtree.get_ngtree("ARP-Table", tree_type="NetDB") 133 | 134 | for en in pc: 135 | ngtree = nglib.ngtree.get_ngtree("ARP Entry", tree_type="NetDB") 136 | 137 | ngtree['firstSeen'] = str(en['firstseen']) 138 | ngtree['lastSeen'] = str(en['lastseen']) 139 | ngtree['ip'] = en['ip'] 140 | ngtree['vrf'] = en['vrf'] 141 | ngtree['mac'] = en['mac'] 142 | ngtree['fqdn'] = en['name'] 143 | ngtree['vendor'] = en['vendor'] 144 | ngtree['switch'] = en['lastswitch'] 145 | ngtree['port'] = en['lastport'] 146 | ngtree['userid'] = en['userID'] 147 | ngtree['vlan'] = en['vlan'] 148 | 149 | nglib.ngtree.add_child_ngtree(pngtree, ngtree) 150 | 151 | return pngtree 152 | -------------------------------------------------------------------------------- /nglib/netdb/switch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """ 32 | NetGrph NetDB Switch Interface 33 | """ 34 | import re 35 | import socket 36 | import logging 37 | import pymysql 38 | import nglib.netdb 39 | import nglib.ngtree 40 | 41 | logger = logging.getLogger(__name__) 42 | 43 | 44 | def get_switch(switch, port='%', hours=720, trunc=True): 45 | """Get All Interface Details for Switch""" 46 | 47 | # Truncated Values to retrieve 48 | tr = ['switch', 'port', 'status', 'description', 'vlan', 'speed', 'duplex'] 49 | 50 | netdb_ses = nglib.netdb.connect_netdb() 51 | lastseen = nglib.netdb.get_lastseen(hours) 52 | 53 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 54 | 55 | # cursor.execute("SELECT * FROM superswitch " 56 | # + "WHERE ip = '{}' AND lastseen > '{}'".format(switch, lastseen)) 57 | 58 | cursor.execute( 59 | "SELECT switchstatus.switch,switchstatus.port,switchstatus.vlan,switchstatus.status," 60 | + "switchstatus.speed,switchstatus.duplex,switchstatus.description," 61 | + "switchstatus.p_uptime,switchstatus.p_minutes,switchstatus.lastup," 62 | + "superswitch.mac,superswitch.ip,superswitch.s_ip,superswitch.s_name," 63 | + "superswitch.name,superswitch.static,superswitch.mac_nd," 64 | + "superswitch.vendor,superswitch.vrf,superswitch.router," 65 | + "superswitch.uptime,superswitch.minutes,superswitch.firstseen," 66 | + "superswitch.lastseen,nacreg.userID,nacreg.firstName,nacreg.lastName," 67 | + "nacreg.role,nd.n_host,nd.n_ip,nd.n_desc,nd.n_model,nd.n_port," 68 | + "nd.n_protocol,nd.n_lastseen,superswitch.s_speed,superswitch.s_ip," 69 | + "superswitch.s_vlan " 70 | + "FROM switchstatus LEFT OUTER JOIN superswitch " 71 | + "ON switchstatus.switch = superswitch.switch " 72 | + "AND switchstatus.port = superswitch.port " 73 | + "AND superswitch.lastseen > '{}' ".format(lastseen) 74 | + "LEFT OUTER JOIN neighbor as nd " 75 | + "ON ( switchstatus.switch = nd.switch AND switchstatus.port = nd.port ) " 76 | + "LEFT OUTER JOIN nacreg ON nacreg.mac = superswitch.mac " 77 | + "WHERE (switchstatus.switch like '{}' AND switchstatus.port like '{}') ".format(switch, port) 78 | + "ORDER BY switchstatus.port" 79 | ) 80 | 81 | pc = cursor.fetchall() 82 | 83 | pngtree = nglib.ngtree.get_ngtree(switch, tree_type="INTs") 84 | 85 | pseen = dict() 86 | 87 | # Gather details from DB in ngtree structure 88 | for en in pc: 89 | ngtree = nglib.ngtree.get_ngtree("INT", tree_type="INT") 90 | 91 | for e in en: 92 | if e in tr and en['port'] not in pseen: 93 | ngtree[e] = en[e] 94 | 95 | if en['port'] not in pseen: 96 | pseen[en['port']] = True 97 | ngtree['Name'] = ngtree['port'] 98 | nglib.ngtree.add_child_ngtree(pngtree, ngtree) 99 | 100 | return pngtree 101 | 102 | def mac(switch, port='%', hours=1): 103 | """Pull the MAC Table on a switch from NetDB, use switch='%' for everything""" 104 | 105 | netdb_ses = nglib.netdb.connect_netdb() 106 | 107 | lastseen = nglib.netdb.get_lastseen(hours) 108 | 109 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 110 | # cursor = netdb_ses.cursor(dictionary=True) 111 | 112 | cursor.execute("SELECT * FROM superswitch " 113 | + "WHERE switch LIKE '{}' AND port LIKE '{}' ".format(switch, port) 114 | + "AND lastseen > '{}'".format(lastseen)) 115 | 116 | pc = cursor.fetchall() 117 | 118 | pngtree = nglib.ngtree.get_ngtree("MAC-Table", tree_type="NetDB") 119 | 120 | for en in pc: 121 | ngtree = nglib.ngtree.get_ngtree("MAC", tree_type="NetDB") 122 | 123 | for f in en: 124 | ngtree[f] = str(en[f]) 125 | 126 | nglib.ngtree.add_child_ngtree(pngtree, ngtree) 127 | 128 | return pngtree 129 | 130 | def count(switch, hours=1): 131 | """ Get the mac address count from a switch """ 132 | 133 | netdb_ses = nglib.netdb.connect_netdb() 134 | 135 | lastseen = nglib.netdb.get_lastseen(hours) 136 | 137 | cursor = netdb_ses.cursor(pymysql.cursors.DictCursor) 138 | 139 | cursor.execute("SELECT count(mac) FROM superswitch " 140 | + "WHERE switch LIKE '{}' ".format(switch) 141 | + "AND lastseen > '{}'".format(lastseen)) 142 | 143 | pc = cursor.fetchall() 144 | 145 | pngtree = nglib.ngtree.get_ngtree("MAC-Count", tree_type="NetDB") 146 | pngtree['switch'] = switch 147 | 148 | for en in pc: 149 | print(en) 150 | pngtree['mac_count'] = en['count(mac)'] 151 | 152 | return pngtree 153 | -------------------------------------------------------------------------------- /nglib/ngtree/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Work with netgrph tree structures 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | # 33 | # 34 | """ 35 | Work with netgrph tree structures 36 | 37 | ngtrees are nested dicts that convert to JSON/YAML 38 | 39 | - Getting an ngtree will create a new unnested ngtree to populate with properties 40 | - Adding a child ngtree will nest an ngtree under a parent 41 | - Adding a parent ngtree will add a special parent ngtree for when you want 42 | the perspective of a certain tree level, but want to add a parent object 43 | 44 | """ 45 | import re 46 | import datetime 47 | import logging 48 | import nglib 49 | from . import export 50 | from . import upgrade 51 | 52 | logger = logging.getLogger(__name__) 53 | 54 | verbose = 0 55 | 56 | 57 | def get_ngtree(name, tree_type="VLAN"): 58 | """Initialize an NGTree""" 59 | 60 | ngtree = dict() 61 | ngtree['Name'] = name 62 | ngtree['_type'] = tree_type 63 | ngtree['_ccount'] = 0 64 | ngtree['data'] = [] 65 | 66 | return ngtree 67 | 68 | def add_child_ngtree(ngtree, cngtree): 69 | """ 70 | Nest a child ngtree under data list in ngtree 71 | """ 72 | 73 | ngtree['_ccount'] = ngtree['_ccount'] + 1 74 | ngtree['data'].append(cngtree) 75 | 76 | def print_ngtree(ngtree, dtree, parent=False, depth=0, lasttree=False): 77 | """ 78 | Recrusively print NGTrees using UTF-8 line drawing characters. If this 79 | causes terminal problems for you and you would prefer an ASCII only mode, 80 | contact me. 81 | 82 | Notes: This code is complicated, even to me. It should probably be rewritten, 83 | but basically, it nests multiple levels of ngtrees and their children in a 84 | pretty output format for use on the CLI. 85 | 86 | get_space_indent() gets output to prepend to lines to form the tree 87 | structure during output. It keeps track of where output is relative to the 88 | rest of the tree structure. 89 | 90 | The dtree dict keeps track of positions on tree to print continuations. 91 | 92 | The close out section needs to be better understood, but when closing out a 93 | child tree, you need to keep pipes from connecting sections below. 94 | 95 | """ 96 | 97 | dtree = dtree.copy() 98 | 99 | # Get indentation spaces variable based on depth 100 | spaces, indent = get_space_indent(depth, dtree) 101 | 102 | # Get indentation spaces variable based on depth 103 | spaces, indent = get_space_indent(depth, dtree) 104 | 105 | # Print Header 106 | 107 | if depth == 0: 108 | indent = "" 109 | 110 | # Last tree terminates with └ 111 | if lasttree: 112 | indent = indent.replace('├', '└') 113 | 114 | # Abbreviate certain types for shorter headers 115 | ngtype = ngtree['_type'] 116 | if ngtype == "VLAN": 117 | ngtype = "" 118 | elif ngtree['_type'] == "Neighbor": 119 | ngtype = "" 120 | else: 121 | ngtype = " " + ngtype 122 | header = " " 123 | header = header.join([ngtype, ngtree['Name']]) 124 | 125 | # Print section header -[header] 126 | if depth == 0: 127 | print("{:}┌─[{:} ]".format(indent, header)) 128 | print("│") 129 | else: 130 | # Headonly for QPATH Results 131 | headonly = True 132 | for en in ngtree: 133 | #if not re.search(r'Name|_type|_ccount', en): 134 | if not re.search(r'Name|_type|_ccount|data', en) \ 135 | or len(ngtree['data']): 136 | 137 | headonly = False 138 | break 139 | if headonly: 140 | print("{:}──[{:} ]".format(indent, header)) 141 | else: 142 | print("{:}┬─[{:} ]".format(indent, header)) 143 | 144 | # Store Children list from data 145 | clist = ngtree['data'] 146 | 147 | # If there are no children, do not indent tree structure 148 | if len(clist) == 0: 149 | dtree.pop(depth, None) 150 | 151 | # Get indentation spaces variable based on current depth 152 | spaces, indent = get_space_indent(depth, dtree) 153 | 154 | # Filter tree of structural data (_ccount etc) 155 | ftree = filter_tree(ngtree) 156 | if nglib.verbose > 1: 157 | ftree = ngtree 158 | 159 | # Print all keys at current depth 160 | # Last one prints special if terminating section 161 | lastcount = len(ftree.keys()) 162 | for key in sorted(ftree.keys()): 163 | lastcount = lastcount - 1 164 | 165 | # If there are children of current tree, then continue tree. 166 | # Otherwise terminate tree with └ 167 | if lastcount or len(clist) > 0: 168 | print("{:}├── {:} : {:}".format(spaces, key, ftree[key])) 169 | else: 170 | print("{:}└── {:} : {:}".format(spaces, key, ftree[key])) 171 | 172 | 173 | # Close out a section with empty line for visual separation 174 | if len(clist) > 0: 175 | spaces = spaces + "│" 176 | print(spaces) 177 | 178 | # Print child trees with recursive call to this function 179 | while len(clist) > 0: 180 | 181 | ctree = clist.pop(0) 182 | 183 | # Continue printing with depth 184 | if len(clist) != 0: 185 | dtree[depth] = 1 186 | 187 | lasttree = False 188 | # End of indentation, un-indent 189 | if len(clist) == 0: 190 | dtree.pop(depth, None) 191 | lasttree = True 192 | 193 | spaces, indent = get_space_indent(depth, dtree) 194 | 195 | # Indent and print child tree recursively 196 | cdepth = depth + 4 197 | print_ngtree(ctree, dtree, depth=cdepth, lasttree=lasttree) 198 | 199 | # Ending section, un-indent 200 | if len(clist) == 0: 201 | dtree.pop(depth, None) 202 | 203 | 204 | def get_space_indent(depth, dtree): 205 | """Returns indentation and spacing strings for building ngtree output""" 206 | 207 | spaces = "" 208 | indent = "" 209 | count = 0 210 | 211 | while count < depth: 212 | count = count + 1 213 | 214 | if count - 1 in dtree.keys(): 215 | #print("Found Dtree at " + str(count-1)) 216 | spaces = spaces + "│" 217 | else: 218 | spaces = spaces + " " 219 | #iline = "-" + iline 220 | if count < depth - 4: 221 | indent = spaces + " " 222 | 223 | indent = indent + "├───" 224 | 225 | return spaces, indent 226 | 227 | 228 | def filter_tree(ngtree): 229 | '''Filter structural data''' 230 | 231 | keys = ngtree.keys() 232 | newtree = dict() 233 | 234 | for key in keys: 235 | if not re.search('(^_)|(^Name$)|(^data$)', key): 236 | newtree[key] = ngtree[key] 237 | #ngtree.pop(key) 238 | 239 | return newtree 240 | -------------------------------------------------------------------------------- /nglib/ngtree/export.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # NetGrph Export Routines 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | # 33 | # 34 | """ 35 | Helper functions to export ngtrees in the right format 36 | """ 37 | import re 38 | import logging 39 | import json 40 | import yaml 41 | import csv 42 | import sys 43 | import nglib.ngtree 44 | 45 | verbose = 0 46 | logger = logging.getLogger(__name__) 47 | 48 | def exp_ngtree(ngtree, rtype): 49 | """Prints or Returns NGTree in Requested format""" 50 | 51 | if rtype == "TREE": 52 | nglib.ngtree.print_ngtree(ngtree, dtree=dict()) 53 | elif rtype == 'QTREE': 54 | exp_qtree(ngtree) 55 | elif rtype == "CSV": 56 | exp_CSV(ngtree) 57 | elif rtype == "CSV2": 58 | exp_CSV(ngtree, level=2) 59 | elif rtype == "JSON": 60 | exp_JSON(ngtree) 61 | elif rtype == "YAML": 62 | exp_YAML(ngtree) 63 | else: 64 | return ngtree 65 | 66 | def exp_JSON(ngtree): 67 | """Prints an ngtree as JSON""" 68 | 69 | print(get_JSON(ngtree)) 70 | 71 | def get_JSON(ngtree): 72 | """Returns an ngtree as JSON Object""" 73 | 74 | jtree = json.dumps(ngtree, indent=2, sort_keys=True) 75 | return jtree 76 | 77 | # Export as YAML 78 | def exp_YAML(ngtree): 79 | """Prints an ngtree as YAML""" 80 | print(get_YAML(ngtree)) 81 | 82 | def get_YAML(ngtree): 83 | """Returns an ngtree as YAML Object""" 84 | ytree = yaml.dump(ngtree, Dumper=yaml.Dumper, default_flow_style=False) 85 | return ytree 86 | 87 | def exp_qtree(ngtree): 88 | """Prints an ngtree with headers only""" 89 | 90 | stree = strip_ngtree(ngtree) 91 | nglib.ngtree.print_ngtree(stree, dtree=dict()) 92 | 93 | def cleanNGTree(ngtree): 94 | """Removes counts from output""" 95 | 96 | cleanND = ngtree.copy() 97 | cleanND.pop('_ccount', None) 98 | return cleanND 99 | 100 | def exp_CSV(ngtree, level=1): 101 | """Flatten NGTREE and dump as CSV, optional two levels deep""" 102 | 103 | fieldnames = [] 104 | if level == 2: 105 | fieldnames.append('_ctype') 106 | fieldnames.append('CName') 107 | 108 | for child in ngtree['data']: 109 | for en in sorted(child.keys()): 110 | if isinstance(en, (int, str)) and en != 'data' \ 111 | and en not in fieldnames: 112 | fieldnames.append(en) 113 | 114 | # Two levels deep 115 | if level == 2: 116 | for gchild in child['data']: 117 | for en in sorted(gchild.keys()): 118 | if isinstance(en, (int, str)) and en != 'data' \ 119 | and en not in fieldnames: 120 | fieldnames.append(en) 121 | 122 | excsv = csv.DictWriter(sys.stdout, fieldnames=fieldnames) 123 | excsv.writeheader() 124 | 125 | for child in ngtree['data']: 126 | entry = dict() 127 | for en in sorted(child.keys()): 128 | if isinstance(en, (int, str)) and not re.search('data|Switches', en): 129 | entry[en] = child[en] 130 | if level == 2: 131 | for gchild in child['data']: 132 | centry = entry.copy() 133 | for en in sorted(gchild.keys()): 134 | if isinstance(en, (int, str)) and not re.search('data|Switches', en): 135 | if en == 'Name': 136 | centry['CName'] = gchild[en] 137 | elif en == '_type': 138 | centry['_ctype'] = gchild[en] 139 | else: 140 | centry[en] = gchild[en] 141 | excsv.writerow(centry) 142 | 143 | else: 144 | excsv.writerow(entry) 145 | 146 | 147 | def strip_ngtree(ngtree, top=True): 148 | """Strips everything but headers from ngtree""" 149 | 150 | newtree = nglib.ngtree.get_ngtree(ngtree['Name'], tree_type=ngtree['_type']) 151 | 152 | for en in ngtree['data']: 153 | newtree['data'].append(strip_ngtree(en, top=False)) 154 | if top: 155 | for en in ngtree: 156 | if en != 'data': 157 | newtree[en] = ngtree[en] 158 | 159 | return newtree 160 | -------------------------------------------------------------------------------- /nglib/ngtree/upgrade.py: -------------------------------------------------------------------------------- 1 | 'Upgrade older ngtrees to newer version' 2 | import logging 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | def upgrade_ngt_v2(ngt): 7 | 'Upgrade ngt structures to version 2 for the API' 8 | 9 | stack = list() 10 | 11 | # Add dictionary to stack 12 | stack.append(ngt) 13 | 14 | # upgrade keys on all dictionaries 15 | for tree in stack: 16 | # Copy NGT and traverse 17 | nt = tree.copy() 18 | for f in nt: 19 | # Upgrade dictionary key 20 | tree.pop(f) 21 | tree[_new_name(f)] = nt[f] 22 | 23 | # Found a nested dict, add to stack 24 | if isinstance(nt[f], dict): 25 | stack.append(nt[f]) 26 | 27 | # Found a nested list 28 | elif isinstance(nt[f], list): 29 | for en in nt[f]: 30 | # nested dict in list, add to stack 31 | if isinstance(en, dict): 32 | stack.append(en) 33 | 34 | return ngt 35 | 36 | 37 | def _new_name(old): 38 | 'Get new name for fields (lowercase, replace spaces with _)' 39 | 40 | nmap = { 41 | 'StandbyRouter': 'standby_router', 42 | 'SecurityLevel': 'security_level', 43 | 'mgmtgroup': 'mgmt_group' 44 | } 45 | 46 | if old in nmap: 47 | return nmap[old] 48 | 49 | old = old.replace(' ', '_') 50 | old = old.lower() 51 | 52 | if old == 'data': 53 | old = 'xdata' 54 | 55 | return old 56 | -------------------------------------------------------------------------------- /nglib/query/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """NetGrph Queries Library (Parent Module)""" 32 | 33 | import csv 34 | import re 35 | import os 36 | import sys 37 | import logging 38 | import functools 39 | import configparser 40 | import ipaddress 41 | import nglib 42 | import nglib.ngtree 43 | import nglib.query.vlan 44 | import nglib.query.dev 45 | import nglib.query.net 46 | import nglib.query.nNode 47 | import nglib.query.path 48 | 49 | 50 | logger = logging.getLogger(__name__) 51 | 52 | def exp_ngtree(ngtree, rtype): 53 | """Prints or Returns NGTree in Requested format""" 54 | 55 | if rtype == "TREE": 56 | nglib.ngtree.print_ngtree(ngtree, dtree=dict()) 57 | elif rtype == 'QTREE': 58 | nglib.ngtree.export.exp_qtree(ngtree) 59 | elif rtype == "CSV": 60 | nglib.ngtree.export.exp_CSV(ngtree) 61 | elif rtype == "JSON": 62 | nglib.ngtree.export.exp_JSON(ngtree) 63 | elif rtype == "YAML": 64 | nglib.ngtree.export.exp_YAML(ngtree) 65 | else: 66 | return ngtree 67 | 68 | def display_mgmt_groups(): 69 | """Print all Management Groups to the Screen""" 70 | 71 | mgmt = nglib.py2neo_ses.cypher.execute( 72 | 'MATCH (s:Switch) RETURN DISTINCT(s.mgmt) as name ORDER BY name') 73 | 74 | if len(mgmt) > 0: 75 | print("Available Groups:") 76 | for s in mgmt.records: 77 | print("> " + str(s.name)) 78 | else: 79 | print("No management groups found in DB") 80 | 81 | 82 | def print_dict_csv(netList): 83 | """Print out List of Dictionary Objects as CSV""" 84 | 85 | netKeys = [] 86 | 87 | # Get Dict keys for CSV Out 88 | for key in sorted(netList[0].keys()): 89 | if key != "__values__": 90 | netKeys.append(key) 91 | 92 | netWriter = csv.writer(sys.stdout) 93 | netWriter.writerow(netKeys) 94 | 95 | # Go through all entries and dump them to CSV 96 | for en in netList: 97 | netValues = [] 98 | for key in sorted(en.keys()): 99 | if key != "__values__": 100 | netValues.append(en[key]) 101 | # Write Values to CSV 102 | netWriter.writerow(netValues) 103 | 104 | 105 | def get_net_filter(group): 106 | """Try to get a filter from the config""" 107 | 108 | try: 109 | gFilter = nglib.config['NetAlertFilter'][group] 110 | return gFilter 111 | except: 112 | raise Exception("No Group Filter Found", group) 113 | 114 | 115 | def check_net_filter(netDict, group=None, nFilter=None): 116 | """ 117 | Filters Networks based on vrf:role (from supernets) 118 | 119 | Pass in a group filter to read from config, or a custom nFilter 120 | 121 | Examples: 122 | nst = all (all networks) 123 | 124 | # default VRF only with null supernet or new access layer supernets 10.177 125 | radonc = default:none|access-private|access-wan 126 | 127 | # EST wants all access layer networks plus all PCI networks 128 | est = default:none|access-private|access-wan pci:all 129 | 130 | # JCI Wants all fwutil networks 131 | jci = fwutil:all 132 | 133 | # COM wants only printer networks 134 | com = default:printer 135 | """ 136 | 137 | if group: 138 | vDict = get_filter_dict(group=group) 139 | elif nFilter: 140 | vDict = get_filter_dict(nFilter=nFilter) 141 | 142 | # Check filter against vDict 143 | for vrf in vDict.keys(): 144 | if netDict['VRF'] == vrf or vrf == "all": 145 | #print("VRF MATCH", vrf, netDict['VRF']) 146 | for role in vDict[vrf]: 147 | if netDict['NetRole'] == role or role == "all": 148 | if nglib.verbose > 1: 149 | logger.debug("VRF and Role MATCH: %s, %s, %s, %s, %s", 150 | group, vrf, netDict['VRF'], netDict['NetRole'], netDict['CIDR']) 151 | return True 152 | 153 | # No supernet role associated (none=None) 154 | elif role == "none" and not netDict['NetRole']: 155 | if nglib.verbose > 1: 156 | logger.debug("VRF and Role MATCH on Null Role: %s, %s, %s, %s, %s", 157 | group, vrf, netDict['VRF'], netDict['NetRole'], netDict['CIDR']) 158 | return True 159 | 160 | # No Match 161 | elif nglib.verbose > 3: 162 | logger.debug("NOMATCH VRF and Role: %s, %s, %s, %s, %s", 163 | group, vrf, netDict['VRF'], netDict['NetRole'], netDict['CIDR']) 164 | 165 | if nglib.verbose > 2: 166 | logger.debug("No Network Match for %s on %s", netDict['CIDR'], group) 167 | return False 168 | 169 | 170 | @functools.lru_cache(maxsize=1) 171 | def get_filter_dict(group=None, nFilter=None): 172 | """Process config for group or custom filter and cache it for each call""" 173 | 174 | vDict = dict() 175 | gFilter = None 176 | 177 | if group: 178 | gFilter = nglib.config['NetAlertFilter'][group] 179 | elif nFilter: 180 | gFilter = nFilter 181 | else: 182 | raise Exception("Must pass in group or filter") 183 | 184 | # Split all VRFs by spaces 185 | vrfFilters = gFilter.rsplit() 186 | 187 | # Process Filters 188 | for f in vrfFilters: 189 | 190 | # Multiple roles specified 191 | if re.search(':', f): 192 | (vrf, roles) = f.split(':') 193 | vDict[vrf] = [] # Empty vDict List 194 | if re.search('|', roles): 195 | rList = roles.split('|') # Split up multiple roles if exists 196 | for r in rList: 197 | vDict[vrf].append(r) # Append multiple roles to List 198 | else: 199 | vDict[vrf].append(roles) # Append single role to list 200 | 201 | # All roles for VRF 202 | else: 203 | vDict[f] = [] 204 | vDict[f].append("all") 205 | 206 | if nglib.verbose > 1: 207 | print("vDict Contents", str(vDict)) 208 | 209 | return vDict 210 | 211 | def universal_text_search(text, vrange, rtype="TREE"): 212 | """ 213 | Try to find what someone is looking for based on a text string 214 | Uses a lot of try/except code, best to debug non-universal before 215 | debugging here. 216 | """ 217 | 218 | if nglib.verbose: 219 | print("Universal Search for", text) 220 | 221 | found = False 222 | 223 | #Look for group filter first 224 | try: 225 | get_net_filter(text) 226 | nglib.query.net.get_networks_on_filter(text, rtype=rtype) 227 | found = True 228 | except: 229 | pass 230 | 231 | if not found: 232 | 233 | # Look for MGMT Group 234 | mgmt = nglib.bolt_ses.run( 235 | 'MATCH (s:Switch {mgmt:{mgmt}}) RETURN DISTINCT(s.mgmt) as name', 236 | {"mgmt": text}) 237 | 238 | for m in mgmt: 239 | nglib.query.vlan.get_vlans_on_group(text, vrange) 240 | found = True 241 | 242 | if not found: 243 | # Look for Device Name 244 | devices = nglib.bolt_ses.run( 245 | 'MATCH (s:Switch {name:{switch}}) RETURN s.name as name', 246 | {"switch": text}) 247 | 248 | for d in devices: 249 | nglib.query.dev.get_device(text, rtype=rtype, vrange=vrange) 250 | found = True 251 | 252 | if not found: 253 | print("Nothing found for Universal Search", text) 254 | -------------------------------------------------------------------------------- /nglib/query/nNode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Neo4j py2neo Node and Edge Methods 4 | # 5 | # Copyright (c) 2016 "Jonathan Yantis" 6 | # 7 | # This file is a part of NetGrph. 8 | # 9 | # This program is free software: you can redistribute it and/or modify 10 | # it under the terms of the GNU Affero General Public License, version 3, 11 | # as published by the Free Software Foundation. 12 | # 13 | # This program is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with this program. If not, see . 20 | # 21 | # As a special exception, the copyright holders give permission to link the 22 | # code of portions of this program with the OpenSSL library under certain 23 | # conditions as described in each individual source file and distribute 24 | # linked combinations including the program with the OpenSSL library. You 25 | # must comply with the GNU Affero General Public License in all respects 26 | # for all of the code used other than as permitted herein. If you modify 27 | # file(s) with this exception, you may extend this exception to your 28 | # version of the file(s), but you are not obligated to do so. If you do not 29 | # wish to do so, delete this exception statement from your version. If you 30 | # delete this exception statement from all source files in the program, 31 | # then also delete it in the license file. 32 | """ 33 | Py2Neo Node Properties (Deprecated) 34 | 35 | """ 36 | import logging 37 | import json 38 | import nglib 39 | 40 | logger = logging.getLogger(__name__) 41 | 42 | 43 | def getJSONProperties(node): 44 | """Returns Properties for a node from JSON as a nested dictionary""" 45 | 46 | json_string = swapQuotes(str(node.properties)) 47 | if nglib.verbose > 1: 48 | print("JSON:", json_string) 49 | parsed_json = json.loads(json_string) 50 | return parsed_json 51 | 52 | 53 | def getLabel(node): 54 | """Returns the label in plaintext for a node""" 55 | 56 | label = node.labels.copy() 57 | label = label.__str__() 58 | label = label.replace('{', '(') 59 | label = label.replace('}', ')') 60 | 61 | return label 62 | 63 | def getEdge(edge): 64 | """Return Edge Type""" 65 | return str(edge.type) 66 | 67 | def getRelationship(edge): 68 | """Get Relationship on edge for printing""" 69 | 70 | snode = edge.start_node 71 | snodeProp = getJSONProperties(snode) 72 | enode = edge.end_node 73 | enodeProp = getJSONProperties(enode) 74 | relation = getLabel(snode) + "{name:" + snodeProp['name'] + "}-[" + getEdge(edge) 75 | relation += "]->" + getLabel(enode) + "{name:" + enodeProp['name'] + "}" 76 | 77 | return relation 78 | 79 | 80 | def swapQuotes(myString): 81 | """Swaps single quote with double quotes""" 82 | 83 | myString = myString.replace("'", '"') 84 | return myString 85 | -------------------------------------------------------------------------------- /nglib/report/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | """Generates Reports on NetGrph Data""" 32 | 33 | import csv 34 | import re 35 | import os 36 | import sys 37 | import logging 38 | import functools 39 | import configparser 40 | import ipaddress 41 | import nglib 42 | import nglib.query 43 | 44 | verbose = 0 45 | logger = logging.getLogger(__name__) 46 | 47 | 48 | def get_vlan_report(vrange, group='.*', report="full", rtype="NGTREE"): 49 | """Generate VLAN Reports""" 50 | 51 | rtypes = ('TREE', 'JSON', 'YAML', 'NGTREE') 52 | 53 | if rtype in rtypes: 54 | 55 | # Full VLAN Report 56 | if report == "full": 57 | logger.info("Query: Generating Full VLAN Report (%s) for %s", vrange, nglib.user) 58 | 59 | # Get all VLANs as NGTree 60 | ngtree = None 61 | if group != '.*': 62 | ngtree = nglib.query.vlan.get_vlans_on_group(group, vrange, rtype="NGTREE") 63 | else: 64 | ngtree = get_vlan_data(vrange, rtype) 65 | if '_ccount' in ngtree.keys(): 66 | nglib.query.exp_ngtree(ngtree, rtype) 67 | return ngtree 68 | else: 69 | print("No Vlans in Range", vrange) 70 | 71 | # Empty VLAN Report 72 | elif report == "empty": 73 | logger.info("Query: Generating Empty VLAN Report (%s) for %s", vrange, nglib.user) 74 | 75 | # Get all VLANs as NGTree 76 | ngtree = get_vlan_data(vrange, rtype) 77 | 78 | # Found some VLANs in Range 79 | if '_ccount' in ngtree: 80 | etree = nglib.ngtree.get_ngtree("Empty VLAN Report", tree_type="VIDs") 81 | clist = ngtree['data'] 82 | 83 | # Find Empty VLANs in NGTree 84 | for cv in clist: 85 | get_empty_vlans(cv, etree) 86 | 87 | # Found Empty VLANs, count and print 88 | if '_ccount' in etree.keys(): 89 | etree['Empty VLAN Count'] = etree['_ccount'] 90 | nglib.query.exp_ngtree(etree, rtype) 91 | return etree 92 | else: 93 | print("No Empty Vlans in Range", vrange) 94 | 95 | else: 96 | print("No Vlans in Range", vrange) 97 | else: 98 | raise Exception("RType Not Supported, use:" + str(rtypes)) 99 | 100 | 101 | def get_empty_vlans(ngtree, etree): 102 | """ 103 | Find all empty VLANs on a VID tree instance 104 | 105 | Notes: Called recursively on any bridge trees and builds etree 106 | """ 107 | 108 | vidlist = ngtree['data'] 109 | 110 | # VID in Bridge Group 111 | for vid in vidlist: 112 | 113 | # No MACs in VLAN 114 | if vid['MAC Count'] == 0: 115 | childlist = vid['data'] 116 | if not childlist: 117 | nglib.ngtree.add_child_ngtree(etree, vid) 118 | else: 119 | get_empty_vlans(vid, etree) 120 | 121 | return etree 122 | 123 | def get_vlan_data(vrange, rtype, group='.*'): 124 | """Get all vlans in a range for reports""" 125 | 126 | allSwitches = True 127 | if rtype == "TREE": 128 | allSwitches = False 129 | 130 | (vlow, vhigh) = nglib.query.vlan.get_vlan_range(vrange) 131 | 132 | vlans = nglib.bolt_ses.run( 133 | 'MATCH(v:VLAN) WHERE toInt(v.vid) >= {vlow} AND toInt(v.vid) <= {vhigh} ' 134 | + 'AND v.mgmt =~ {group} ' 135 | + 'RETURN DISTINCT v.vid AS vid ORDER BY toInt(vid)', 136 | {"vlow": vlow, "vhigh": vhigh, "group": group}) 137 | 138 | pngtree = nglib.ngtree.get_ngtree("Report", tree_type="VIDs") 139 | 140 | for v in vlans: 141 | vtree = nglib.query.vlan.search_vlan_id(v['vid'], allSwitches=allSwitches) 142 | nglib.ngtree.add_child_ngtree(pngtree, vtree) 143 | return pngtree 144 | 145 | 146 | def get_vrf_report(vrf, rtype="NGTREE"): 147 | """ 148 | Get a report on vrfs that match regex 149 | """ 150 | rtypes = ('TREE', 'JSON', 'YAML', 'NGTREE') 151 | 152 | if rtype in rtypes: 153 | logger.info("Query: Generating VRF Report (%s) for %s", vrf, nglib.user) 154 | 155 | vrfs = nglib.bolt_ses.run( 156 | 'MATCH(v:VRF) WHERE v.name =~ {vrf} ' 157 | + 'RETURN v.name AS name ORDER BY name', 158 | {"vrf": vrf}) 159 | 160 | ngtree = nglib.ngtree.get_ngtree("Report", tree_type="VRFs") 161 | ngtree['VRF Regex'] = vrf 162 | 163 | # Process VRFs 164 | tree_count = 0 165 | vrflist = [] 166 | for v in vrfs: 167 | tree_count += 1 168 | cngtree = nglib.query.net.get_networks_on_filter(nFilter=v["name"], rtype="NGTREE") 169 | if cngtree: 170 | vrflist.append(v["name"]) 171 | devlist = nglib.query.dev.get_devlist_vrf(v["name"]) 172 | cngtree["Routers"] = devlist 173 | tree_count += 1 174 | nglib.ngtree.add_child_ngtree(ngtree, cngtree) 175 | 176 | # Return output 177 | if not tree_count: 178 | print("No VRFs found on regex:", vrf, file=sys.stderr) 179 | elif tree_count == 1: 180 | nglib.query.exp_ngtree(cngtree, rtype) 181 | return ngtree 182 | else: 183 | ngtree["VRFs"] = vrflist 184 | nglib.query.exp_ngtree(ngtree, rtype) 185 | return ngtree 186 | else: 187 | raise Exception("RType Not Supported, use:" + str(rtypes)) 188 | 189 | 190 | def get_dev_report(dev, group=".*", trunc=False, rtype="NGTREE"): 191 | """ 192 | Get all devices on a regex 193 | 194 | Options: trunc==True returns truncated list 195 | """ 196 | 197 | rtypes = ('TREE', 'JSON', 'YAML', 'NGTREE') 198 | 199 | if rtype in rtypes: 200 | logger.info("Query: Generating Device Report (%s) for %s", dev, nglib.user) 201 | 202 | devices = nglib.bolt_ses.run( 203 | 'MATCH(s:Switch) WHERE s.mgmt =~ {mgmt} AND s.name =~ {dev} ' 204 | + 'RETURN s.name AS name, s.mgmt AS mgmt, ' 205 | + 's.location AS location, s.model AS model, s.version AS version, ' 206 | + 's.distance AS distance, s.Platform AS platform, s.FQDN as FQDN ' 207 | + 'ORDER BY name', 208 | {'dev': dev, 'mgmt': group}) 209 | 210 | ngtree = nglib.ngtree.get_ngtree("Report", tree_type="DEVS") 211 | ngtree['Device Regex'] = dev 212 | 213 | for d in devices: 214 | if d["mgmt"]: 215 | if trunc: 216 | ct = nglib.ngtree.get_ngtree(d['name'], tree_type="DEV") 217 | ct['Distance'] = d['distance'] 218 | ct['Location'] = d['location'] 219 | ct['MGMT Group'] = d['mgmt'] 220 | ct['Model'] = d['model'] 221 | ct['Version'] = d['version'] 222 | ct['Platform'] = d['platform'] 223 | ct['FQDN'] = d['FQDN'] 224 | nglib.ngtree.add_child_ngtree(ngtree, ct) 225 | else: 226 | cngtree = nglib.query.dev.get_device(d["name"]) 227 | if cngtree: 228 | nglib.ngtree.add_child_ngtree(ngtree, cngtree) 229 | 230 | # Found Devices, count and print 231 | if '_ccount' in ngtree.keys(): 232 | ngtree['Device Count'] = ngtree['_ccount'] 233 | nglib.query.exp_ngtree(ngtree, rtype) 234 | return ngtree 235 | else: 236 | print("No Devices found on regex:", dev) 237 | 238 | else: 239 | raise Exception("RType Not Supported, use:" + str(rtypes)) 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /ngreport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # 4 | # Copyright (c) 2016 "Jonathan Yantis" 5 | # 6 | # This file is a part of NetGrph. 7 | # 8 | # This program is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU Affero General Public License, version 3, 10 | # as published by the Free Software Foundation. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with this program. If not, see . 19 | # 20 | # As a special exception, the copyright holders give permission to link the 21 | # code of portions of this program with the OpenSSL library under certain 22 | # conditions as described in each individual source file and distribute 23 | # linked combinations including the program with the OpenSSL library. You 24 | # must comply with the GNU Affero General Public License in all respects 25 | # for all of the code used other than as permitted herein. If you modify 26 | # file(s) with this exception, you may extend this exception to your 27 | # version of the file(s), but you are not obligated to do so. If you do not 28 | # wish to do so, delete this exception statement from your version. If you 29 | # delete this exception statement from all source files in the program, 30 | # then also delete it in the license file. 31 | # 32 | # 33 | """ngreport Generates Reports from the NetGrph Database""" 34 | import os 35 | import re 36 | import argparse 37 | import nglib 38 | import nglib.report 39 | 40 | # Default Config File Location 41 | config_file = '/etc/netgrph.ini' 42 | alt_config = './docs/netgrph.ini' 43 | 44 | # Test/Dev Config 45 | dirname = os.path.dirname(os.path.realpath(__file__)) 46 | if re.search(r'\/dev$', dirname): 47 | config_file = 'netgrphdev.ini' 48 | elif re.search(r'\/test$', dirname): 49 | config_file = "netgrphdev.ini" 50 | 51 | parser = argparse.ArgumentParser() 52 | 53 | parser = argparse.ArgumentParser(prog='ngreport', 54 | description='Generate Reports from NetGrph') 55 | 56 | parser.add_argument("-vrf", metavar='name', 57 | help="Generate a Report on a VRF (.* for all)", 58 | type=str) 59 | parser.add_argument("-vlans", help="VLAN ID Report (combine with -vra and -e)", 60 | action="store_true") 61 | 62 | parser.add_argument("-vrange", metavar='1[-4096]', help="VLAN Range (default 1-1999)", 63 | type=str) 64 | parser.add_argument("-dev", metavar=".*", help="Report on Network Devices in regex", 65 | type=str) 66 | parser.add_argument("-output", metavar='TREE', 67 | help="Return Format: TREE, TABLE, CSV, JSON, YAML", type=str) 68 | parser.add_argument("-empty", 69 | help="Only Return Empty VLANs (requires NetDB)", 70 | action="store_true") 71 | parser.add_argument("--conf", metavar='file', help="Alternate Config File", type=str) 72 | parser.add_argument("--debug", help="Set debugging level", type=int) 73 | parser.add_argument("-v", help="Verbose Output", action="store_true") 74 | 75 | args = parser.parse_args() 76 | 77 | # Alternate Config File 78 | if args.conf: 79 | config_file = args.conf 80 | 81 | # Test configuration exists 82 | if not os.path.exists(config_file): 83 | if not os.path.exists(alt_config): 84 | raise Exception("Configuration File not found", config_file) 85 | else: 86 | config_file = alt_config 87 | 88 | verbose = 0 89 | if args.v: 90 | verbose = 1 91 | if args.debug: 92 | verbose = args.debug 93 | 94 | # Default VLAN Range 95 | if not args.vrange: 96 | args.vrange = "1-1999" 97 | if args.output: 98 | args.output = args.output.upper() 99 | 100 | # Setup Globals 101 | nglib.verbose = verbose 102 | 103 | # Initialize Library 104 | nglib.init_nglib(config_file) 105 | 106 | if args.vlans: 107 | report = "full" 108 | if args.empty: 109 | report = "empty" 110 | 111 | rtype = "TREE" 112 | if args.output: 113 | rtype = args.output 114 | 115 | nglib.report.get_vlan_report(args.vrange, report=report, rtype=rtype) 116 | 117 | elif args.vrf: 118 | rtype = "TREE" 119 | if args.output: 120 | rtype = args.output 121 | 122 | nglib.report.get_vrf_report(args.vrf, rtype=rtype) 123 | 124 | elif args.dev: 125 | rtype = "TREE" 126 | if args.output: 127 | rtype = args.output 128 | 129 | nglib.report.get_dev_report(args.dev, rtype=rtype) 130 | 131 | else: 132 | parser.print_help() 133 | print() 134 | -------------------------------------------------------------------------------- /ngtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | # 32 | """ 33 | Test Database functions for errors 34 | """ 35 | import re 36 | import sys 37 | import subprocess 38 | 39 | ngcmd = './netgrph.py' + ' ' 40 | upngcmd = './ngupdate.py' + ' ' 41 | ngrepcmd = './ngreport.py' + ' ' 42 | 43 | # Query Tests (cmd, result) 44 | qtest = dict() 45 | qtest['-dev core1'] = 'Child Neighbors' 46 | qtest['-ip 10.9.46.1'] = 'Gateway' 47 | qtest['-fp 10.1.120.50 8.8.8.8'] = 'ExternalFW' 48 | qtest['-net 10.9.46.0/23 -o YAML'] = 'Router: xyz2mdf' 49 | qtest['-nlist test_group'] = '10.9.46.0/23' 50 | qtest['-group Core'] = 'core1' 51 | qtest['-vid 120'] = '10.1.120.0/23' 52 | qtest['-vtree ABC-120'] = '10.1.120.0/23' 53 | qtest['-ip 10.9.136.1 -o JSON'] = '"IP": "10.9.136.1"' 54 | qtest['-ip 10.9.136.1 -o YAML'] = 'IP: 10.9.136.1' 55 | qtest['-sp xyz2mdf abc4mdf'] = 'core1' 56 | qtest['-rp 10.1.108.50 10.1.20.50'] = 'core1' 57 | qtest['-p 10.1.120.50 8.8.8.8'] = 'Description : Default Route' 58 | 59 | # Production tests 60 | ptest = dict() 61 | ptest['-dev core1'] = 'Child Neighbors' 62 | ptest['-ip 10.32.1.1'] = 'Gateway' 63 | ptest['-fp 10.33.1.1 8.8.8.8'] = 'ExternalFW' 64 | ptest['-net 10.32.0.0/17'] = 'wireless-GUEST' 65 | ptest['-nlist security'] = '10.32.0.0/17' 66 | ptest['-group Core'] = 'core1' 67 | ptest['-vid 641'] = '10.32.0.0/17' 68 | ptest['-vtree Core-641'] = '10.32.0.0/17' 69 | ptest['-ip 10.32.1.1 -o JSON'] = '"IP": "10.32.1.1"' 70 | ptest['-sp mdcmdf hvt404mdf'] = 'core1' 71 | ptest['-rp 10.33.1.100 10.26.76.1'] = 'core1' 72 | ptest['-p 10.26.72.142 10.34.72.24'] = 'L4-FW FwutilFW' 73 | ptest['10.26.72.142 10.34.72.24'] = 'L4-FW FwutilFW' 74 | 75 | 76 | # NetDB 77 | #ptest['-p 10.26.72.142 10.28.6.27'] = 'Link rVLANs' 78 | 79 | rtest = dict() 80 | #rtest['-vlans -empty -vrange 200'] = 'v6test' 81 | rtest['-vlans -vrange 120 -o yaml'] = 'Root: abc4mdf' 82 | rtest['-v -dev "xyz.*" -o json'] = '"Parent Switch": "core1"' 83 | 84 | prtest = dict() 85 | prtest['-vlans -empty -vrange 200'] = 'v6test' 86 | 87 | # Update Tests (check for errors) 88 | utest = ('-ivrf', '-id', '-ind', '-inet', '-isnet', '-iasa', '-ivlan', '-uvlan', '-ild') 89 | 90 | def run_query(cmd, option, result): 91 | """Run Queries for testing""" 92 | 93 | runcmd = cmd + option 94 | proc = subprocess.Popen([runcmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE, 95 | shell=True, universal_newlines=True) 96 | (out, err) = proc.communicate() 97 | 98 | if re.search(r'Traceback', err): 99 | print(err, file=sys.stderr) 100 | return False 101 | elif result and re.search(result, out): 102 | return True 103 | elif not result: 104 | return True 105 | else: 106 | return False 107 | 108 | def test_dev_updates(): 109 | print("Testing Update Routines") 110 | 111 | for ut in utest: 112 | print(ut) 113 | assert(run_query(upngcmd, ut, None)) == True 114 | 115 | def test_dev_queries(): 116 | """netgrph query testing""" 117 | 118 | print("Testing Query Routines") 119 | 120 | for key in qtest.keys(): 121 | print(key) 122 | assert(run_query(ngcmd, key, qtest[key])) == True 123 | 124 | for key in rtest.keys(): 125 | print(key) 126 | assert(run_query(ngrepcmd, key, rtest[key])) == True 127 | 128 | def test_prod_updates(): 129 | test_dev_updates() 130 | 131 | def test_prod_queries(): 132 | global qtest 133 | global rtest 134 | qtest = ptest 135 | rtest = prtest 136 | 137 | test_dev_queries() 138 | 139 | 140 | -------------------------------------------------------------------------------- /requirements-client.txt: -------------------------------------------------------------------------------- 1 | requests 2 | pyyaml 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Python Requirements 2 | ciscoconfparse 3 | pylint 4 | pyyaml 5 | pymysql 6 | py2neo==2.0.8 7 | neo4j-driver==1.0.2 8 | Flask 9 | Flask-HTTPAuth 10 | flask_limiter 11 | passlib 12 | sqlalchemy 13 | flask_sqlalchemy 14 | requests 15 | uwsgi 16 | -------------------------------------------------------------------------------- /test/__pycache__/testng.cpython-34-PYTEST.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantisj/netgrph/0ebc72efbd970e0ce76d44d91a27d167103633b5/test/__pycache__/testng.cpython-34-PYTEST.pyc -------------------------------------------------------------------------------- /test/apisrv: -------------------------------------------------------------------------------- 1 | ../apisrv/ -------------------------------------------------------------------------------- /test/bitwise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import ipaddress 4 | 5 | 6 | def int_to_bin(num, bits=32): 7 | r = '' 8 | while bits: 9 | r = ('1' if num&1 else '0') + r 10 | bits = bits - 1 11 | num = num >> 1 12 | print(r) 13 | 14 | 15 | myip = sys.argv[1] 16 | 17 | 18 | ip = ipaddress.IPv4Address(myip) 19 | i = int.from_bytes(ip.packed, byteorder='big') 20 | 21 | print(ip.packed) 22 | print(i) 23 | int_to_bin(i) 24 | 25 | -------------------------------------------------------------------------------- /test/csv/allnets.csv: -------------------------------------------------------------------------------- 1 | Subnet,VLAN,VRF,Router,Gateway,MGMT Group,Description,P2P,Standby 2 | 10.3.108.10/31,2108,default,core1,10.3.108.10,Core,abc4mdf,True,False 3 | 10.3.108.10/31,2323,guest,core1,10.3.108.10,Core,abc4mdf-core1 vrf guest,True,False 4 | 10.3.108.10/31,2360,perim,core1,10.3.108.10,Core,core1-abc4mdf vrf perim,True,False 5 | 10.3.108.10/31,2460,utility,core1,10.3.108.10,Core,core1-abc4mdf vrf utility,True,False 6 | 10.3.108.20/31,3108,default,core2,10.3.108.20,Core,abc4mdf,True,True 7 | 10.3.108.20/31,3323,guest,core2,10.3.108.20,Core,abc4mdf-core2 vrf guest,True,True 8 | 10.3.108.20/31,3360,perim,core2,10.3.108.20,Core,core2-abc4mdf vrf perim,True,True 9 | 10.3.108.20/31,3460,utility,core2,10.3.108.20,Core,core2-abc4mdf vrf utility,True,True 10 | 10.4.108.0/24,1,default,abc4mdf,10.4.108.1,ABC,None,False,False 11 | 10.1.108.0/23,108,default,abc4mdf,10.1.108.50,ABC,None,False,False 12 | 10.1.120.0/23,120,default,abc4mdf,10.1.120.50,ABC,None,False,False 13 | 10.6.64.0/22,260,default,abc4mdf,10.6.64.50,ABC,ABC-Utility,False,False 14 | 10.9.8.0/23,270,guest,abc4mdf,10.9.8.1,ABC,guest,False,False 15 | 10.9.136.0/23,271,perim,abc4mdf,10.9.136.1,ABC,reg,False,False 16 | 10.9.200.0/23,272,perim,abc4mdf,10.9.200.1,ABC,deadend,False,False 17 | 10.8.64.0/22,280,default,abc4mdf,10.8.64.50,ABC,None,False,False 18 | 10.14.64.0/22,340,utility,abc4mdf,10.14.64.50,ABC,ABC-SECURE-UTILITY,False,False 19 | 10.7.108.0/23,408,default,abc4mdf,10.7.108.50,ABC,None,False,False 20 | 10.7.120.0/23,420,default,abc4mdf,10.7.120.50,ABC,None,False,False 21 | 10.3.108.10/31,2108,default,abc4mdf,10.3.108.11,ABC,None,True,False 22 | 10.3.108.10/31,2323,guest,abc4mdf,10.3.108.11,ABC,abc4mdf-core1 vrf guest,True,False 23 | 10.3.108.10/31,2360,perim,abc4mdf,10.3.108.11,ABC,abc4mdf-core1 vrf perim,True,False 24 | 10.3.108.10/31,2460,utility,abc4mdf,10.3.108.11,ABC,abc4mdf-core1 vrf utility,True,False 25 | 10.3.108.20/31,3108,default,abc4mdf,10.3.108.21,ABC,None,True,False 26 | 10.3.108.20/31,3323,guest,abc4mdf,10.3.108.21,ABC,abc4mdf-core1 vrf guest,True,False 27 | 10.3.108.20/31,3360,perim,abc4mdf,10.3.108.21,ABC,abc4mdf-core2 vrf perim,True,False 28 | 10.3.108.20/31,3460,utility,abc4mdf,10.3.108.21,ABC,abc4mdf-core2 vrf utlility,True,False 29 | 10.3.20.10/31,2020,default,core1,10.3.20.10,Core,XYZ2mdf EALERT,True,False 30 | 10.3.20.10/31,2372,guest,core1,10.3.20.10,Core,xyz2mdf vrf guest,True,False 31 | 10.3.20.10/31,2373,perim,core1,10.3.20.10,Core,xyz2mdf-core1-vrf-perim,True,False 32 | 10.3.20.10/31,2474,utility,core1,10.3.20.10,Core,core1-xyz vrf utility,True,False 33 | 10.3.20.20/31,3020,default,core2,10.3.20.20,Core,XYZ2MDF P2P,True,True 34 | 10.3.20.20/31,3372,guest,core2,10.3.20.20,Core,XYZ2MDF-core2-vrf-perim,True,True 35 | 10.3.20.20/31,3474,utility,core2,10.3.20.20,Core,core2-xyz vrf utility,True,True 36 | 10.4.20.0/24,1,default,xyz2mdf,10.4.20.1,XYZ,None,False,False 37 | 10.1.20.0/23,20,default,xyz2mdf,10.1.20.50,XYZ,None,False,False 38 | 10.1.206.0/23,206,default,xyz2mdf,10.1.206.50,XYZ,None,False,False 39 | 10.6.188.0/22,260,default,xyz2mdf,10.6.188.1,XYZ,None,False,False 40 | 10.9.46.0/23,270,guest,xyz2mdf,10.9.46.1,XYZ,guest,False,False 41 | 10.8.96.0/22,280,default,xyz2mdf,10.8.96.1,XYZ,None,False,False 42 | 10.14.188.0/22,340,utility,xyz2mdf,10.14.188.50,XYZ,None,False,False 43 | 10.7.206.0/23,406,default,xyz2mdf,10.7.206.50,XYZ,None,False,False 44 | 10.7.20.0/23,420,default,xyz2mdf,10.7.20.50,XYZ,None,False,False 45 | 10.3.20.10/31,2020,default,xyz2mdf,10.3.20.11,XYZ,None,True,False 46 | 10.3.20.10/31,2372,guest,xyz2mdf,10.3.20.11,XYZ,XYZ2mdf-core1 vrf guest,True,False 47 | 10.3.20.10/31,2373,perim,xyz2mdf,10.3.20.11,XYZ,XYZ2mdf-core1 vrf perim,True,False 48 | 10.3.20.10/31,2474,utility,xyz2mdf,10.3.20.11,XYZ,XYZ-core1 vrf utility,True,False 49 | 10.3.20.20/31,3020,default,xyz2mdf,10.3.20.21,XYZ,None,True,False 50 | 10.3.20.20/31,3372,guest,xyz2mdf,10.3.20.21,XYZ,XYZ2mdf-core1 vrf guest,True,False 51 | 10.3.20.20/31,3373,perim,xyz2mdf,10.3.20.21,XYZ,None,True,False 52 | 10.3.20.20/31,3474,utility,xyz2mdf,10.3.20.21,XYZ,XYZ-core2 vrf utility,True,False 53 | 10.5.20.0/24,630,perim,core1,10.5.20.50,Core,FW,False,False 54 | 10.5.10.0/24,631,default,core1,10.5.10.50,Core,None,False,False 55 | 10.5.20.0/24,630,perim,core2,10.5.20.50,Core,FW,False,True 56 | 10.5.10.0/24,631,default,core2,10.5.10.50,Core,None,False,True 57 | 10.1.0.224/29,1095,perim,core1,10.1.0.225,Core,External-FW,False,False 58 | 10.1.0.224/29,1099,outside,core1,10.1.0.226,Core,None,False,False 59 | -------------------------------------------------------------------------------- /test/csv/allvlans.csv: -------------------------------------------------------------------------------- 1 | MGMT,VID,VName,Switch,STP 2 | Core,2108,core-abc4mdf,core1,0 3 | Core,2323,core1-abcmdf-guest-vrf,core1,0 4 | Core,2360,core1-abc4mdf-vrf-perim,core1,0 5 | Core,2460,core1-abc4mdf-utility,core1,0 6 | Core,3108,core2-abc4mdf,core2,0 7 | Core,3323,core2-abcmdf-guest-vrf,core2,0 8 | Core,3360,core2-abc4mdf-vrf-perim,core2,0 9 | Core,3460,core1-abc4mdf-utility,core2,0 10 | ABC,108,Abc,abc4mdf,24576 11 | ABC,108,Abc-TestNet,abc2e1sw1,0 12 | ABC,108,Abc-TestNet,abc2sw1,0 13 | ABC,108,Abc-TestNet,abc437sw1,0 14 | ABC,108,Abc-TestNet,abc437sw2,0 15 | ABC,108,Abc-TestNet,abc4sw1,0 16 | ABC,120,Abc2,abc4mdf,24576 17 | ABC,120,Abc2-TestNet,abc2e1sw1,0 18 | ABC,120,Abc2-TestNet,abc2sw1,0 19 | ABC,120,Abc2-TestNet,abc437sw1,0 20 | ABC,120,Abc2-TestNet,abc437sw2,0 21 | ABC,120,Abc2-TestNet,abc4sw1,0 22 | ABC,1246,vendor-span,abc2e1sw1,0 23 | ABC,1246,vendor-span,abc4mdf,0 24 | ABC,2108,core1-abc4mdf-default,abc4mdf,0 25 | ABC,2323,abcmdf-core1-guest-vrf,abc4mdf,0 26 | ABC,2360,abc4mdf-core1-vrf-perim,abc4mdf,0 27 | ABC,2460,core1-abc4mdf-utility,abc4mdf,0 28 | ABC,260,Utility-Devices,abc2e1sw1,0 29 | ABC,260,Utility-Devices,abc2sw1,0 30 | ABC,260,Utility-Devices,abc437sw1,0 31 | ABC,260,Utility-Devices,abc437sw2,0 32 | ABC,260,Utility-Devices,abc4sw1,0 33 | ABC,260,utility,abc4mdf,24576 34 | ABC,280,Printers,abc2e1sw1,0 35 | ABC,280,Printers,abc2sw1,0 36 | ABC,280,Printers,abc437sw1,0 37 | ABC,280,Printers,abc437sw2,0 38 | ABC,280,Printers,abc4mdf,24576 39 | ABC,280,Printers,abc4sw1,0 40 | ABC,3108,core2-abc4mdf-default,abc4mdf,0 41 | ABC,3323,abcmdf-core2-guest-vrf,abc4mdf,0 42 | ABC,3360,abc4mdf-core2-vrf-perim,abc4mdf,0 43 | ABC,34,PerimeterNet,abc2e1sw1,0 44 | ABC,34,PerimeterNet,abc2sw1,0 45 | ABC,34,PerimeterNet,abc437sw1,0 46 | ABC,34,PerimeterNet,abc437sw2,0 47 | ABC,34,PerimeterNet,abc4sw1,0 48 | ABC,340,SECURE-UTILITY,abc2e1sw1,0 49 | ABC,340,SECURE-UTILITY,abc2sw1,0 50 | ABC,340,SECURE-UTILITY,abc437sw1,0 51 | ABC,340,SECURE-UTILITY,abc437sw2,0 52 | ABC,340,SECURE-UTILITY,abc4mdf,0 53 | ABC,340,SECURE-UTILITY,abc4sw1,0 54 | ABC,3460,core2-abc4mdf-utility,abc4mdf,0 55 | ABC,408,abc-v108-voice,abc2e1sw1,0 56 | ABC,408,abc-v108-voice,abc2sw1,0 57 | ABC,408,abc-v108-voice,abc437sw1,0 58 | ABC,408,abc-v108-voice,abc437sw2,0 59 | ABC,408,abc-v108-voice,abc4mdf,24576 60 | ABC,408,abc-v108-voice,abc4sw1,0 61 | ABC,420,abc-v120-voice,abc2e1sw1,0 62 | ABC,420,abc-v120-voice,abc2sw1,0 63 | ABC,420,abc-v120-voice,abc437sw1,0 64 | ABC,420,abc-v120-voice,abc437sw2,0 65 | ABC,420,abc-v120-voice,abc4mdf,24576 66 | ABC,420,abc-v120-voice,abc4sw1,0 67 | ABC,499,abc-utility-voice,abc2e1sw1,0 68 | ABC,499,abc-utility-voice,abc2sw1,0 69 | ABC,499,abc-utility-voice,abc437sw1,0 70 | ABC,499,abc-utility-voice,abc437sw2,0 71 | ABC,499,abc-utility-voice,abc4mdf,24576 72 | ABC,499,abc-utility-voice,abc4sw1,0 73 | XYZ,20,XYZ-Date,xyz1sw1,0 74 | XYZ,20,XYZ-Date,xyz1sw2,0 75 | XYZ,20,XYZ-Date,xyz2mdf,24576 76 | XYZ,20,XYZ-Date,xyz2sw1,0 77 | XYZ,20,XYZ-Date,xyz3sw1,0 78 | XYZ,20,XYZ-Date,xyz4sw1,0 79 | XYZ,20,XYZ-Date,xyz4sw2,0 80 | XYZ,2000,NONAME,xyz2mdf,0 81 | XYZ,2020,XYZ2mdf-core1,xyz2mdf,0 82 | XYZ,206,XYZ-date2,xyz1sw1,0 83 | XYZ,206,XYZ-date2,xyz1sw2,0 84 | XYZ,206,XYZ-date2,xyz2mdf,24576 85 | XYZ,206,XYZ-date2,xyz2sw1,0 86 | XYZ,206,XYZ-date2,xyz3sw1,0 87 | XYZ,206,XYZ-date2,xyz4sw1,0 88 | XYZ,206,XYZ-date2,xyz4sw2,0 89 | XYZ,2372,XYZ-core1-vrf-guest,xyz2mdf,0 90 | XYZ,2373,XYZ-core1-vrf-perim,xyz2mdf,0 91 | XYZ,2474,core1-XYZ-utility,xyz2mdf,0 92 | XYZ,260,[XYZ-utility],xyz1sw1,0 93 | XYZ,260,[XYZ-utility],xyz1sw2,0 94 | XYZ,260,[XYZ-utility],xyz2mdf,24576 95 | XYZ,260,[XYZ-utility],xyz2sw1,0 96 | XYZ,260,[XYZ-utility],xyz3sw1,0 97 | XYZ,260,[XYZ-utility],xyz4sw1,0 98 | XYZ,260,[XYZ-utility],xyz4sw2,0 99 | XYZ,280,[XYZ_printers],xyz1sw1,0 100 | XYZ,280,[XYZ_printers],xyz1sw2,0 101 | XYZ,280,[XYZ_printers],xyz2mdf,24576 102 | XYZ,280,[XYZ_printers],xyz2sw1,0 103 | XYZ,280,[XYZ_printers],xyz3sw1,0 104 | XYZ,280,[XYZ_printers],xyz4sw1,0 105 | XYZ,280,[XYZ_printers],xyz4sw2,0 106 | XYZ,3020,XYZ2mdf-core2,xyz2mdf,0 107 | XYZ,3372,XYZ-core2-vrf-guest,xyz2mdf,0 108 | XYZ,3373,XYZ-core2-vrf-perim,xyz2mdf,0 109 | XYZ,340,Utility-SEC,xyz1sw1,0 110 | XYZ,340,Utility-SEC,xyz1sw2,0 111 | XYZ,340,Utility-SEC,xyz2mdf,0 112 | XYZ,340,Utility-SEC,xyz2sw1,0 113 | XYZ,340,Utility-SEC,xyz3sw1,0 114 | XYZ,340,Utility-SEC,xyz4sw1,0 115 | XYZ,340,Utility-SEC,xyz4sw2,0 116 | XYZ,3474,core2-XYZ-utility,xyz2mdf,0 117 | XYZ,406,XYZ-406-voice,xyz1sw1,0 118 | XYZ,406,XYZ-406-voice,xyz1sw2,0 119 | XYZ,406,XYZ-406-voice,xyz2mdf,24576 120 | XYZ,406,XYZ-406-voice,xyz2sw1,0 121 | XYZ,406,XYZ-406-voice,xyz3sw1,0 122 | XYZ,406,XYZ-406-voice,xyz4sw1,0 123 | XYZ,406,XYZ-406-voice,xyz4sw2,0 124 | XYZ,420,XYZ-420-voice,xyz1sw1,0 125 | XYZ,420,XYZ-420-voice,xyz1sw2,0 126 | XYZ,420,XYZ-420-voice,xyz2mdf,24576 127 | XYZ,420,XYZ-420-voice,xyz2sw1,0 128 | XYZ,420,XYZ-420-voice,xyz3sw1,0 129 | XYZ,420,XYZ-420-voice,xyz4sw1,0 130 | XYZ,420,XYZ-420-voice,xyz4sw2,0 131 | Core,2020,core1-XYZ2mdf,core1,0 132 | Core,2372,core1-XYZ2mdf-vrf-guest,core1,0 133 | Core,2373,core1-XYZ2mdf-vrf-perim,core1,0 134 | Core,2474,core1-xyz-utility,core1,0 135 | Core,3020,core2-XYZ2mdf,core2,0 136 | Core,3372,core2-XYZ-vrf-guest,core2,0 137 | Core,3373,core2-XYZ-vrf-perim,core2,0 138 | Core,3474,core2-xyz-utility,core2,0 139 | Core,1246,SPAN-Net,core1,20480 140 | Core,1246,SPAN-Net,core2,20480 141 | XYZ,1246,vendor-span,xyz2mdf,0 142 | XYZ,1246,vendor-span,xyz4sw1,0 143 | -------------------------------------------------------------------------------- /test/csv/devices.csv: -------------------------------------------------------------------------------- 1 | Device,FQDN,MgmtGroup,Type 2 | core1,core1.testnetwork.com,Core,Primary 3 | core2,core2.testnetwork.com,Core,Standby 4 | abc2e1sw1,abc2e1sw1.testnetwork.com,ABC,Switch 5 | abc2sw1,abc2sw1.testnetwork.com,ABC,Switch 6 | abc437sw1,abc437sw1.testnetwork.com,ABC,Switch 7 | abc437sw2,abc437sw2.testnetwork.com,ABC,Switch 8 | abc4mdf,abc4mdf.testnetwork.com,ABC,Primary 9 | abc4sw1,abc4sw1.testnetwork.com,ABC,Switch 10 | xyz2mdf,xyz2mdf.testnetwork.com,XYZ,Primary 11 | xyz1sw1,xyz1sw1.testnetwork.com,XYZ,Switch 12 | xyz1sw2,xyz1sw2.testnetwork.com,XYZ,Switch 13 | xyz2sw1,xyz2sw1.testnetwork.com,XYZ,Switch 14 | xyz3sw1,xyz3sw1.testnetwork.com,XYZ,Switch 15 | xyz4sw1,xyz4sw1.testnetwork.com,XYZ,Switch 16 | xyz4sw2,xyz4sw2.testnetwork.com,XYZ,Switch 17 | -------------------------------------------------------------------------------- /test/csv/devinfo.csv: -------------------------------------------------------------------------------- 1 | Device,Location 2 | -------------------------------------------------------------------------------- /test/csv/firewalls.csv: -------------------------------------------------------------------------------- 1 | Name,Interface,Description,Security-Level,IP,Hostname,Log-Index 2 | PerimeterFW,Vlan630,VRF Perim side of FW,0,None,fsm,firewalls 3 | PerimeterFW,Vlan631,Internal Network side of FW,100,None,fsm,firewalls 4 | -------------------------------------------------------------------------------- /test/csv/links.csv: -------------------------------------------------------------------------------- 1 | Port,Switch,channel,desc,native,vlans 2 | Eth10/16,core1,0,abc4mdf EALERT,2108,"1246,2108,2323,2360,2460" 3 | Eth10/16,core2,0,abc4mdf EALERT,3108,"1246,3108,3323,3360,3460" 4 | Gi1/0/49,abc2e1sw1,0,,1,"1-1005,1246" 5 | Gi1/0/50,abc2e1sw1,0,,1,1-4096 6 | Gi1/0/51,abc2e1sw1,0,,1,1-4096 7 | Gi1/0/52,abc2e1sw1,0,,1,1-4096 8 | Gi1/0/49,abc2sw1,0,,1,1-1005 9 | Gi1/0/50,abc2sw1,0,,1,1-1005 10 | Gi1/0/51,abc2sw1,0,,1,1-1005 11 | Gi1/0/52,abc2sw1,0,,1,1-1005 12 | Gi1/0/49,abc437sw1,0,,1,1-1005 13 | Gi1/0/50,abc437sw1,0,,1,1-4096 14 | Gi1/0/51,abc437sw1,0,,1,1-1005 15 | Gi1/0/52,abc437sw1,0,,1,1-4096 16 | Gi2/0/49,abc437sw1,0,,1,1-4096 17 | Gi2/0/50,abc437sw1,0,,1,1-4096 18 | Gi2/0/51,abc437sw1,0,,1,1-4096 19 | Gi2/0/52,abc437sw1,0,,1,1-4096 20 | Gi3/0/49,abc437sw1,0,,1,1-4096 21 | Gi3/0/50,abc437sw1,0,,1,1-4096 22 | Gi3/0/51,abc437sw1,0,,1,1-4096 23 | Gi3/0/52,abc437sw1,0,,1,1-4096 24 | Gi4/0/49,abc437sw1,0,,1,1-4096 25 | Gi4/0/50,abc437sw1,0,,1,1-4096 26 | Gi4/0/51,abc437sw1,0,,1,1-4096 27 | Gi4/0/52,abc437sw1,0,,1,1-4096 28 | Gi1/0/49,abc437sw2,0,,1,1-1005 29 | Gi1/0/50,abc437sw2,0,,1,1-4096 30 | Gi1/0/51,abc437sw2,0,,1,1-4096 31 | Gi1/0/52,abc437sw2,0,,1,1-4096 32 | Gi2/0/49,abc437sw2,0,,1,1-4096 33 | Gi2/0/50,abc437sw2,0,,1,1-4096 34 | Gi2/0/51,abc437sw2,0,,1,1-4096 35 | Gi2/0/52,abc437sw2,0,,1,1-4096 36 | Gi1/0/25,abc4mdf,0,abc4sw1,1,1-1005 37 | Gi1/0/26,abc4mdf,0,abc2e1sw1,1,"1-1005,1246" 38 | Gi1/0/27,abc4mdf,0,,1,1-1005 39 | Gi1/0/28,abc4mdf,0,core1 EALERT,2108,"1246,2108,2323,2360,2460" 40 | Gi2/0/25,abc4mdf,0,abc437sw1,1,1-1005 41 | Gi2/0/26,abc4mdf,0,abc2sw1,1,1-1005 42 | Gi2/0/27,abc4mdf,0,,1,1-1005 43 | Gi2/0/28,abc4mdf,0,core2 EALERT,3108,"1246,3108,3323,3360,3460" 44 | Gi3/0/25,abc4mdf,0,abc437sw2,1,1-1005 45 | Gi3/0/26,abc4mdf,0,,1,1-1005 46 | Gi3/0/27,abc4mdf,0,,1,1-1005 47 | Gi1/0/49,abc4sw1,0,,1,1-1005 48 | Gi1/0/50,abc4sw1,0,,1,1-4096 49 | Gi1/0/51,abc4sw1,0,,1,1-4096 50 | Gi1/0/52,abc4sw1,0,,1,1-4096 51 | Gi2/0/49,abc4sw1,0,,1,1-4096 52 | Gi2/0/50,abc4sw1,0,,1,1-4096 53 | Gi2/0/51,abc4sw1,0,,1,1-4096 54 | Gi2/0/52,abc4sw1,0,,1,1-4096 55 | Eth10/5,core1,0,xyz2mdf EALERT,2020,"2020,2372-2373,2474" 56 | Eth10/5,core2,0,xyz2mdf EALERT,3020,"3020,3372-3373,3474" 57 | Gi1/0/1,xyz2mdf,10,,1,1-1005 58 | Gi1/0/2,xyz2mdf,11,,1,1-1005 59 | Gi1/0/3,xyz2mdf,20,,1,1-1005 60 | Gi1/0/4,xyz2mdf,31,,1,1-1005 61 | Gi1/0/5,xyz2mdf,41,,1,1-1005 62 | Gi1/0/6,xyz2mdf,42,,1,1-1005 63 | Gi1/0/7,xyz2mdf,0,,1,1-4000 64 | Gi1/0/8,xyz2mdf,0,,1,1-4000 65 | Gi1/0/9,xyz2mdf,0,,1,1-4000 66 | Gi1/0/10,xyz2mdf,0,,1,1-4000 67 | Gi1/0/11,xyz2mdf,0,,1,1-4000 68 | Gi1/0/12,xyz2mdf,0,core1 EALERT,2020,"2020,2372,2373,2474" 69 | Gi2/0/1,xyz2mdf,10,,1,1-1005 70 | Gi2/0/2,xyz2mdf,11,,1,1-1005 71 | Gi2/0/3,xyz2mdf,20,,1,1-1005 72 | Gi2/0/4,xyz2mdf,31,,1,1-1005 73 | Gi2/0/5,xyz2mdf,41,,1,1-1005 74 | Gi2/0/6,xyz2mdf,42,,1,1-1005 75 | Gi2/0/7,xyz2mdf,0,,1,1-4000 76 | Gi2/0/8,xyz2mdf,0,,1,1-4000 77 | Gi2/0/9,xyz2mdf,0,,1,1-4000 78 | Gi2/0/10,xyz2mdf,0,,1,1-4000 79 | Gi2/0/11,xyz2mdf,0,,1,1-4000 80 | Gi2/0/12,xyz2mdf,0,core2 EALERT,3020,"3020,3372,3373,3474" 81 | Te5/1,xyz1sw1,1,,1,1-1005 82 | Te5/2,xyz1sw1,1,,1,1-1005 83 | Te5/3,xyz1sw1,0,,1,1-1005 84 | Te5/4,xyz1sw1,0,,1,1-1005 85 | Gi1/1/1,xyz1sw2,1,,1,1-1005 86 | Gi1/1/2,xyz1sw2,0,,1,1-1005 87 | Gi2/1/1,xyz1sw2,1,,1,1-1005 88 | Te5/1,xyz2sw1,1,,1,1-1005 89 | Te5/2,xyz2sw1,1,,1,1-1005 90 | Te5/3,xyz2sw1,0,,1,1-1005 91 | Te5/4,xyz2sw1,0,,1,1-1005 92 | Te5/1,xyz3sw1,1,,1,1-1005 93 | Te5/2,xyz3sw1,1,,1,1-1005 94 | Te5/3,xyz3sw1,0,,1,1-1005 95 | Te5/4,xyz3sw1,0,,1,1-1005 96 | Te5/1,xyz4sw1,1,,1,1-1005 97 | Te5/2,xyz4sw1,1,,1,1-1005 98 | Te5/3,xyz4sw1,0,,1,1-1005 99 | Te5/4,xyz4sw1,0,,1,1-1005 100 | Gi1/1/1,xyz4sw2,1,,1,1-1005 101 | Gi1/1/2,xyz4sw2,1,,1,1-1005 102 | Gi1/1/3,xyz4sw2,0,,1,1-1005 103 | Gi1/1/4,xyz4sw2,0,,1,1-1005 104 | Eth7/26,core1,100,core2 EALERT,2001,"16,105,249,464,630-637,642-643,650-652,706,710,805,990,1246,1601-1610,1701,1803-1804,2001,2301,2315,2396,2404,2421-2422,2495,2519,3001,3238,3442" 105 | Eth8/26,core1,100,core2 EALERT,2001,"16,105,249,464,630-637,642-643,650-652,706,710,805,990,1246,1601-1610,1701,1803-1804,2001,2301,2315,2396,2404,2421-2422,2495,2519,3001,3238,3442" 106 | Eth7/8,core2,100,core1 EALERT,2001,"16,105,249,464,630-637,642-643,650-652,701,706,710,805,990,1246,1601-1610,1701,1803-1804,2001,2301,2315,2396,2404,2421-2422,2495,2519,3001,3238,3442" 107 | Eth8/26,core2,100,core1 EALERT,2001,"16,105,249,464,630-637,642-643,650-652,701,706,710,805,990,1246,1601-1610,1701,1803-1804,2001,2301,2315,2396,2404,2421-2422,2495,2519,3001,3238,3442" 108 | -------------------------------------------------------------------------------- /test/csv/nd.csv: -------------------------------------------------------------------------------- 1 | LocalName,LocalPort,RemoteName,RemotePort 2 | core2,Eth10/16,abc4mdf,Gi2/0/28 3 | core1,Eth10/16,abc4mdf,Gi1/0/28 4 | abc2e1sw1,Gi1/0/49,abc4mdf,Gi1/0/26 5 | abc437sw2,Gi1/0/49,abc4mdf,Gi3/0/25 6 | abc437sw1,Gi1/0/51,abc4mdf,Gi2/0/25 7 | abc4sw1,Gi1/0/49,abc4mdf,Gi1/0/25 8 | abc2sw1,Gi1/0/49,abc4mdf,Gi2/0/26 9 | abc4mdf,Gi1/0/26,abc2e1sw1,Gi1/0/49 10 | abc4mdf,Gi1/0/28,core1,Eth10/16 11 | abc4mdf,Gi2/0/28,core2,Eth10/16 12 | abc4mdf,Gi1/0/25,abc4sw1,Gi1/0/49 13 | abc4mdf,Gi2/0/25,abc437sw1,Gi1/0/51 14 | abc4mdf,Gi2/0/26,abc2sw1,Gi1/0/49 15 | abc4mdf,Gi3/0/25,abc437sw2,Gi1/0/49 16 | core2,Eth7/8,core1,Eth8/26 17 | core2,Eth8/26,core1,Eth7/26 18 | core1,Eth7/26,core2,Eth8/26 19 | core1,Eth8/26,core2,Eth7/8 20 | core2,Eth10/5,xyz2mdf,Gi2/0/12 21 | core1,Eth10/5,xyz2mdf,Gi1/0/12 22 | xyz1sw1,Te5/2,xyz2mdf,Gi2/0/1 23 | xyz1sw1,Te5/1,xyz2mdf,Gi1/0/1 24 | xyz2sw1,Te5/2,xyz2mdf,Gi2/0/3 25 | xyz2sw1,Te5/1,xyz2mdf,Gi1/0/3 26 | xyz4sw1,Te5/2,xyz2mdf,Gi2/0/5 27 | xyz4sw1,Te5/1,xyz2mdf,Gi1/0/5 28 | xyz3sw1,Te5/2,xyz2mdf,Gi2/0/4 29 | xyz3sw1,Te5/1,xyz2mdf,Gi1/0/4 30 | xyz4sw2,Gi1/1/2,xyz2mdf,Gi2/0/6 31 | xyz4sw2,Gi1/1/1,xyz2mdf,Gi1/0/6 32 | xyz1sw2,Gi2/1/1,xyz2mdf,Gi1/0/2 33 | xyz1sw2,Gi1/1/1,xyz2mdf,Gi2/0/2 34 | xyz2mdf,Gi1/0/12,core1,Eth10/5 35 | xyz2mdf,Gi2/0/12,core2,Eth10/5 36 | xyz2mdf,Gi2/0/4,xyz3sw1,Te5/2 37 | xyz2mdf,Gi1/0/4,xyz3sw1,Te5/1 38 | xyz2mdf,Gi1/0/2,xyz1sw2,Gi2/1/1 39 | xyz2mdf,Gi2/0/3,xyz2sw1,Te5/2 40 | xyz2mdf,Gi1/0/3,xyz2sw1,Te5/1 41 | xyz2mdf,Gi2/0/2,xyz1sw2,Gi1/1/1 42 | xyz2mdf,Gi2/0/1,xyz1sw1,Te5/2 43 | xyz2mdf,Gi1/0/1,xyz1sw1,Te5/1 44 | xyz2mdf,Gi2/0/6,xyz4sw2,Gi1/1/2 45 | xyz2mdf,Gi1/0/6,xyz4sw2,Gi1/1/1 46 | xyz2mdf,Gi2/0/5,xyz4sw1,Te5/2 47 | xyz2mdf,Gi1/0/5,xyz4sw1,Te5/1 48 | -------------------------------------------------------------------------------- /test/csv/supernets.csv: -------------------------------------------------------------------------------- 1 | cidr,role,description,secure 2 | 10.9.0.0/16,nac,NAC Registration,50 3 | 10.14.0.0/16,pci,Secure Utility Network,150 4 | -------------------------------------------------------------------------------- /test/csv/vrfs.csv: -------------------------------------------------------------------------------- 1 | default,100,Default Routing Table 2 | guest,10,Guest Network 3 | outside,1,External Networks 4 | perim,25,Perimeter DMZ Network 5 | utility,150,Utility Network 6 | test,1,Test Empty VRF 7 | -------------------------------------------------------------------------------- /test/first_import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ./ngupdate.py -v --dropDatabase 3 | sleep 2 4 | ./ngupdate.py -ivrf -v 5 | ./ngupdate.py -id -v 6 | ./ngupdate.py -ind -v 7 | ./ngupdate.py -v -inet --ignoreNew 8 | ./ngupdate.py -isnet -v 9 | ./ngupdate.py -ivlan -v --ignoreNew 10 | ./ngupdate.py -uvlan -v 11 | ./ngupdate.py -ifw -v 12 | ./ngupdate.py -v -ifile ./cypher/buildfw.cyp 13 | ./ngupdate.py -v -ifile ./cypher/constraints.cyp 14 | ./ngupdate.py -full -v 15 | ./ngupdate.py -v -ifile ./cypher/buildfw.cyp 16 | ./test/test_full.sh 17 | -------------------------------------------------------------------------------- /test/ngclient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # Copyright (c) 2016 "Jonathan Yantis" 4 | # 5 | # This file is a part of NetGrph. 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU Affero General Public License, version 3, 9 | # as published by the Free Software Foundation. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU Affero General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Affero General Public License 17 | # along with this program. If not, see . 18 | # 19 | # As a special exception, the copyright holders give permission to link the 20 | # code of portions of this program with the OpenSSL library under certain 21 | # conditions as described in each individual source file and distribute 22 | # linked combinations including the program with the OpenSSL library. You 23 | # must comply with the GNU Affero General Public License in all respects 24 | # for all of the code used other than as permitted herein. If you modify 25 | # file(s) with this exception, you may extend this exception to your 26 | # version of the file(s), but you are not obligated to do so. If you do not 27 | # wish to do so, delete this exception statement from your version. If you 28 | # delete this exception statement from all source files in the program, 29 | # then also delete it in the license file. 30 | # 31 | # 32 | """ 33 | NetGrph API Client 34 | """ 35 | import sys 36 | import requests 37 | import json 38 | import nglib.ngtree.export 39 | 40 | 41 | user = 'yantisj' 42 | passwd = 'testapi' 43 | url = 'http://localhost:4096' 44 | 45 | output = 'JSON' 46 | 47 | if len(sys.argv) > 1: 48 | output = sys.argv[1] 49 | 50 | #qlist = ['/netgrph/api/v1.1/devs?group=MDC', '/netgrph/api/v1.1/devs?group=CON&full=1', \ 51 | # '/netgrph/api/v1.1/devs/waringsw1'] 52 | 53 | qlist = ['/netgrph/api/v1.1/vlans?vrange=200-205', '/netgrph/api/v1.1/devs?full=1&search=cr3.*'] 54 | 55 | 56 | for q in qlist: 57 | r = requests.get(url + q, auth=(user, passwd), verify=False) 58 | 59 | if r.status_code == 200: 60 | response = r.json() 61 | nglib.ngtree.export.exp_ngtree(response, output) 62 | else: 63 | print("Request Error:", r.status_code, r.text) 64 | -------------------------------------------------------------------------------- /test/nglib: -------------------------------------------------------------------------------- 1 | ../nglib/ -------------------------------------------------------------------------------- /test/pre-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pylint3 ./nglib/ netgrph.py ngupdate.py > ./log/pylint.log 3 | less ./log/pylint.log 4 | -------------------------------------------------------------------------------- /test/set_neo4j_password.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Sets password to "your_passwd" for automated testing 4 | # 5 | curl -H "Content-Type: application/json" -X POST -d '{"password":"your_passwd"}' -u neo4j:neo4j http://localhost:7474/user/neo4j/password 6 | -------------------------------------------------------------------------------- /test/test_full.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | # Check for Ubuntu Python3 4 | export pytestcmd="py.test-3" 5 | 6 | # Revert to default pytest 7 | if ! type "$pytestcmd" 2> /dev/null; then 8 | export pytestcmd="py.test" 9 | fi 10 | 11 | if [ "$1" = "prod" ]; then 12 | $pytestcmd --resultlog=/tmp/pytest.log -k test_prod ngtest.py 13 | else 14 | $pytestcmd --resultlog=/tmp/pytest.log -k test_dev ngtest.py 15 | fi 16 | 17 | -------------------------------------------------------------------------------- /test/test_queries.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | export pytestcmd="py.test-3" 4 | 5 | if ! type "$pytestcmd" 2> /dev/null; then 6 | export pytestcmd="py.test" 7 | fi 8 | 9 | if [ "$1" = "prod" ]; then 10 | $pytestcmd --resultlog=/tmp/pytest.log ngtest.py::test_prod_queries 11 | else 12 | $pytestcmd --resultlog=/tmp/pytest.log ngtest.py::test_dev_queries 13 | fi 14 | -------------------------------------------------------------------------------- /test/testapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2016 Jonathan Yantis 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | # this software and associated documentation files (the "Software"), to deal in 6 | # the Software without restriction, including without limitation the rights to 7 | # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 8 | # the Software, and to permit persons to whom the Software is furnished to do so, 9 | # subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 16 | # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 18 | # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 19 | # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 20 | # 21 | """ 22 | Test the API Application 23 | """ 24 | import os 25 | import builtins 26 | import re 27 | from base64 import b64encode 28 | import unittest 29 | import tempfile 30 | import time 31 | 32 | headers = { 33 | 'Authorization': 'Basic %s' % b64encode(b"testuser:testpass").decode("ascii") 34 | } 35 | 36 | # Default Config File Location 37 | config_file = '/etc/netgrph.ini' 38 | alt_config = './docs/netgrph.ini' 39 | 40 | # Test/Dev Config 41 | dirname = os.path.dirname(os.path.realpath(__file__)) 42 | if re.search(r'\/dev/test$', dirname): 43 | config_file = './netgrphdev.ini' 44 | 45 | # Test configuration exists 46 | if not os.path.exists(config_file): 47 | if not os.path.exists(alt_config): 48 | raise Exception("Configuration File not found", config_file) 49 | else: 50 | config_file = alt_config 51 | 52 | # Import API 53 | builtins.apisrv_CONFIG = config_file 54 | import apisrv 55 | 56 | class APITestCase(unittest.TestCase): 57 | """ API Testing Cases""" 58 | def setUp(self): 59 | apisrv.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + tempfile.mkstemp()[1] 60 | apisrv.app.config['TESTING'] = True 61 | self.app = apisrv.app.test_client() 62 | 63 | with apisrv.app.app_context(): 64 | apisrv.db.create_all() 65 | try: 66 | apisrv.user.add_user("testuser", "testpass") 67 | except Exception: 68 | pass 69 | 70 | def tearDown(self): 71 | apisrv.db.drop_all() 72 | 73 | def test_index(self): 74 | print('Testing Connectivity..') 75 | rv = self.app.get('/') 76 | assert b'Authentication required' in rv.data 77 | 78 | def test_info(self): 79 | print('Testing Authentication..') 80 | rv = self.app.get('/netgrph/api/v1.0/info?test=GETTEST', headers=headers) 81 | assert b'GET test' in rv.data 82 | 83 | def test_paths(self): 84 | print("Testing Paths..") 85 | rv = self.app.get('/netgrph/api/v1.1/path?src=10.26.10.10&dst=10.34.10.10', headers=headers) 86 | assert b'L4-GW' in rv.data 87 | assert b'L3-PATH' in rv.data 88 | assert b'L3-GW' in rv.data 89 | 90 | rv = self.app.get('/netgrph/api/v1.1/spath?dst=mdc.*&src=hvt.*', headers=headers) 91 | assert b'"distance": 4' in rv.data 92 | 93 | def test_vlans(self): 94 | print("Testing VLANs..") 95 | rv = self.app.get('/netgrph/api/v1.1/vlans?vrange=250-300', headers=headers) 96 | assert b'"VLAN ID"' in rv.data 97 | rv = self.app.get('/netgrph/api/v1.1/vlans/200', headers=headers) 98 | assert b'"VLAN ID"' in rv.data 99 | 100 | def test_nets(self): 101 | print("Testing Nets..") 102 | rv = self.app.get('/netgrph/api/v1.1/nets?cidr=10.0.0.0-11', headers=headers) 103 | assert b'"VLAN"' in rv.data 104 | 105 | 106 | if __name__ == '__main__': 107 | unittest.main() 108 | time.sleep(0.25) 109 | -------------------------------------------------------------------------------- /test/testrand.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ Test Random IP's for errors""" 3 | import subprocess 4 | 5 | ip1 = open('/tmp/ip1.txt', 'r') 6 | ip2 = open('/tmp/ip2.txt', 'r') 7 | 8 | verbose = False 9 | 10 | for e1, e2 in zip(ip1, ip2): 11 | e1 = e1.strip() 12 | e2 = e2.strip() 13 | #print(e1, e2) 14 | 15 | cmd = './netgrph.py ' + e1 + ' ' + e2 16 | 17 | proc = subprocess.Popen( 18 | [cmd], 19 | stdout=subprocess.PIPE, 20 | shell=True, 21 | universal_newlines=True) 22 | 23 | (out, err) = proc.communicate() 24 | 25 | if err: 26 | print(cmd, err) 27 | elif out and verbose: 28 | print(out) 29 | -------------------------------------------------------------------------------- /test/uwsgi-api.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export NGLIB_config_file="./netgrphdev.ini" 3 | uwsgi --socket 0.0.0.0:9000 --protocol=http -w wsgi-api -H ./venv/ 4 | -------------------------------------------------------------------------------- /wsgi-api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ Run API Server as UWSGI App""" 3 | 4 | from apisrv import app as application 5 | 6 | if __name__ == "__main__": 7 | application.run() 8 | --------------------------------------------------------------------------------