Skip to content

Commit

Permalink
Merge pull request #73 from v1tal3/development
Browse files Browse the repository at this point in the history
Development v1.3.5
  • Loading branch information
matt852 authored Apr 5, 2018
2 parents ef016c6 + 6215d2e commit e62cb29
Show file tree
Hide file tree
Showing 71 changed files with 1,690 additions and 1,253 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.pyc
*.log
*.db
*.vscode
app/log/
log/*
scripts_bank/logs/*
Expand Down
19 changes: 16 additions & 3 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_script import Manager
from scripts_bank.netboxAPI import NetboxHost
from data_handler import DataHandler
from log_handler import LogHandler
from ssh_handler import SSHHandler
from celery import Celery


app = Flask(__name__, instance_relative_config=True)
app.config.from_object('config')
app.config.from_pyfile('settings.py', silent=True)
db = SQLAlchemy(app)
Bootstrap(app)
try:
netbox = NetboxHost(app.config['NETBOXSERVER'])
datahandler = DataHandler(app.config['DATALOCATION'],
netboxURL=app.config['NETBOXSERVER'])
except KeyError:
netbox = NetboxHost("''")
datahandler = DataHandler('local')

logger = LogHandler(app.config['SYSLOGFILE'])

sshhandler = SSHHandler()

# Celery
celery = Celery(app.name, broker=app.config['CELERY_BROKER_URL'], backend=app.config['CELERY_RESULT_BACKEND'])
celery.conf.update(app.config)

from app import views, models

Expand Down
277 changes: 277 additions & 0 deletions app/data_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
#!/usr/bin/python
import app
import requests
from requests.exceptions import ConnectionError
import csv
from sqlalchemy.exc import IntegrityError, InvalidRequestError

from netaddr import IPAddress, core
from .device_classes import deviceType


class DataHandler(object):
"""Handler object for data sources."""

def __init__(self, source, netboxURL=None):
"""Data handler initialization function."""
self.source = source
self.url = netboxURL

def addHostToDB(self, hostname, ipv4_addr, type, ios_type, local_creds):
"""Add host to database. Returns True if successful."""
try:
host = app.models.Host(hostname=hostname, ipv4_addr=ipv4_addr,
type=type.capitalize(),
ios_type=ios_type,
local_creds=local_creds)
app.db.session.add(host)
# This enables pulling ID for newly inserted host
app.db.session.flush()
app.db.session.commit()
except (IntegrityError, InvalidRequestError) as e:
app.db.session.rollback()
return False, 0, e

try:
app.logger.write_log("Added new host %s to database" % (host.hostname))
return True, host.id, None
except Exception as e:
return False, 0, e

def importHostsToDB(self, csvImport):
"""Import hosts to database.
Returns True if successful
Format: Hostname,IPAddress,DeviceType,IOSType
"""
reader = csv.reader(csvImport.strip().splitlines())
errors = []
hosts = []
for row in reader:
error = {}

# Input validation checks
if len(row) < 4:
error = {'hostname': row[0], 'error': "Invalid number of fields in entry"}
errors.append(error)
continue

try:
IPAddress(row[1])
except core.AddrFormatError:
error = {'hostname': row[0], 'error': "Invalid IP address"}
errors.append(error)
continue

if row[2].lower().strip() not in ("switch", "router", "firewall"):
error = {'hostname': row[0], 'error': "Invalid device type"}
errors.append(error)
continue

ios_type = self.getOSType(row[3])
if ios_type.lower() == "error":
error = {'hostname': row[0], 'error': "Invalid OS type"}
errors.append(error)
continue

if app.models.Host.query.filter_by(hostname=row[0]).first():
error = {'hostname': row[0], 'error': "Duplicate hostname in database"}
errors.append(error)
continue

if app.models.Host.query.filter_by(ipv4_addr=row[1]).first():
error = {'hostname': row[0], 'error': "Duplicate IPv4 address in database"}
errors.append(error)
continue

# Initial validation checks completed successfully. Import into DB
try:
if row[4].strip().lower() == 'true':
local_creds = True
else:
local_creds = False
except IndexError:
local_creds = False

try:
# TODO could probably use self.addHostToDB
host = app.models.Host(hostname=row[0].strip(),
ipv4_addr=row[1],
type=row[2].capitalize(),
ios_type=ios_type,
local_creds=local_creds)
app.db.session.add(host)
app.db.session.flush()
# Do this last, as we only want to add the host to var 'hosts' if it was fully successful
hosts.append({"id": host.id, "hostname": row[0],
"ipv4_addr": row[1]})
except (IntegrityError, InvalidRequestError):
app.db.session.rollback()

# Only commit once for all hosts added
try:
app.db.session.commit()
except (IntegrityError, InvalidRequestError):
app.db.session.rollback()

return hosts, errors

def getOSType(self, os):
"""Process OS Type.
:i (input)
returns os
"""
# TO DO consider returning None instead of Error?
if self.source == 'netbox':
try:
r = requests.get(self.url + '/api/dcim/device-types/' + str(os))
except ConnectionError:
app.logger.write_log("Connection error trying to connect to " + self.url)
return "error"
if r.status_code == requests.codes.ok:

try:
os = r.json()['custom_fields']['Netconfig_OS']['label'].strip()
except KeyError:
return "error"
else:
return "error"

os = os.lower()
if os == 'ios':
return "cisco_ios"
elif os == 'ios-xe':
return "cisco_xe"
elif os == 'nx-os':
return "cisco_nxos"
elif os == 'asa':
return "cisco_asa"
else:
return "error"

def deleteHostInDB(self, x):
"""Remove host from database.
Returns True if successful.
x is the host ID
"""
# IDEA change Netbox Netconfig field for "deleting"

try:
host = app.models.Host.query.filter_by(id=x).first()
app.db.session.delete(host)
app.db.session.commit()
app.logger.write_log('deleted host %s in database' % (host.hostname))
return True
except IntegrityError as err:
app.logger.write_log('unable to delete host %s in database' % (host.hostname))
app.logger.write_log(err)
return False

def getHosts(self):
"""Get certain number of devices in database."""
data = []

if self.source == 'local':

for host in app.models.Host.query.order_by(app.models.Host.hostname).all():

# TO DO consider adding this to the database?
h = host.__dict__
h['source'] = "local"

data.append(h)

elif self.source == 'netbox':
try:
r = requests.get(self.url + '/api/dcim/devices/?limit=0')
except ConnectionError:
app.logger.write_log("Connection error trying to connect to " + self.url)
return data

if r.status_code == requests.codes.ok:

for d in r.json()['results']:
if (d['custom_fields']['Netconfig'] and d['custom_fields']['Netconfig']['label'] == 'Yes'):

os_type = self.getOSType(d['device_type']['id'])
host = {"id": d['id'], "hostname": d['name'],
"ipv4_addr": d['primary_ip']['address'].split('/')[0],
"type": d['device_type']['model'],
"ios_type": os_type,
"source": "netbox",
"local_creds": False}

data.append(host)

return data

def getHostByID(self, x):
"""Get device by ID, regardless of data store location.
Support local database or Netbox inventory.
Does not return SSH session.
x = host id
"""
# TO DO consider merging with getHosts

if self.source == 'local':
# TO DO handle downstream to use a dictionary not a model
host = app.models.Host.query.filter_by(id=x).first()
try:
host = host.__dict__
except AttributeError:
host = {}

elif self.source == 'netbox':
try:
r = requests.get(self.url + '/api/dcim/devices/' + str(x))
except ConnectionError:
app.logger.write_log("Connection error trying to connect to " + self.url)
return None

if r.status_code == requests.codes.ok:
d = r.json()

os_type = self.getOSType(d['device_type']['id'])
host = {"id": d['id'], "hostname": d['name'],
"ipv4_addr": d['primary_ip']['address'].split('/')[0],
"type": d['device_type']['model'],
"ios_type": os_type,
"local_creds": False}
else:
return None

# Get host class based on device type
return deviceType.DeviceHandler(id=host['id'], hostname=host['hostname'],
ipv4_addr=host['ipv4_addr'], type=host['type'],
ios_type=host['ios_type'],
local_creds=host['local_creds'])

def editHostInDatabase(self, id, hostname, ipv4_addr, hosttype, ios_type, local_creds, local_creds_updated):
"""Edit device in database.
This is only supported when using the local database.
"""
# IDEA modify existing Netbox devices?

if self.source == 'local':
try:
host = app.models.Host.query.filter_by(id=id).first()
if hostname:
host.hostname = hostname
if ipv4_addr:
host.ipv4_addr = ipv4_addr
if hosttype:
host.type = hosttype
if ios_type:
host.ios_type = ios_type
if local_creds_updated:
host.local_creds = local_creds
app.db.session.commit()
return True
except:
return False
else:
return False
41 changes: 29 additions & 12 deletions app/device_classes/device_definitions/base_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,52 @@ def __del__(self):
"""Deletion function."""
pass

def save_config_on_device(self, activeSession):
"""Return results from saving configuration on device."""
return activeSession.save_config()

def reset_session_mode(self, activeSession):
"""Check if existing SSH session is in config mode.
If so, exits config mode.
"""
if activeSession.check_config_mode():
self.exit_config_mode(activeSession)
# Return True since session was originally in config mode
if activeSession.exit_config_mode():
# Return True if successful
return True
# Return False if session is not in config mode
return False
else:
# Return False if session is not in config mode
return False

def revert_session_mode(self, activeSession, originalState):
"""Revert SSH session to config mode if it was previously in config mode.
Not currently used.
"""
if originalState and not activeSession.check_config_mode():
self.enter_config_mode(activeSession)
activeSession.enter_config_mode()
elif activeSession.check_config_mode() and not originalState:
self.exit_config_mode()
activeSession.exit_config_mode()

def run_ssh_command(self, command, activeSession):
"""Execute single command on device using existing SSH session."""
# Exit config mode if existing session is currently in config mode
self.reset_session_mode(activeSession)

# Run command and return command output
return activeSession.send_command(command)
# Run command
result = activeSession.send_command(command)
# Run check for invalid input detected, etc
if "Invalid input detected" in result:
# Command failed, possibly due to being in configuration mode. Exit config mode
activeSession.exit_config_mode()
# Try to retrieve command results again
try:
result = self.run_ssh_command('show ip interface brief', activeSession)
# If command still failed, return nothing
if "Invalid input detected" in result:
return self.cleanup_ios_output('', '')
except:
# If failure to access SSH channel or run command, return nothing
return self.cleanup_ios_output('', '')

# Return command output
return result

def run_ssh_config_commands(self, cmdList, activeSession):
"""Execute configuration commands on device.
Expand Down
Loading

0 comments on commit e62cb29

Please sign in to comment.