-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 8ded0a4
Showing
99 changed files
with
565,403 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/.venv | ||
/.vscode | ||
/__pycache__ |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
/.venv | ||
/.vscode | ||
/data | ||
__pycache__/ | ||
*.pyc | ||
__pycache__ | ||
/*code* | ||
todo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
FROM python:3.7 | ||
|
||
WORKDIR /fastapi | ||
|
||
ADD requirements.txt /fastapi/requirements.txt | ||
RUN pip install -r /fastapi/requirements.txt | ||
|
||
COPY ./api /fastapi/api | ||
COPY ./lib /fastapi/lib | ||
COPY ./client/dist /fastapi/client/dist | ||
COPY ./rq-workers /usr/src/workers | ||
|
||
ENV PYTHONPATH="/fastapi:${PYTHONPATH}" | ||
|
||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# Cisco VOIP Phone Info Web Application Server | ||
The VOIP Phone Info server is a application that collects IP phone data from your Cisco VOIP environment. | ||
|
||
The key features are: | ||
|
||
* **Phone configuration data**: Pulls high level configuration data (MAC/Description/Device Pool/etc) from Cisco Unified Communications Manager (CUCM) via AXL | ||
* **Phone registration data**: Pulls real-time registration data (IP address, firmware, registration time stamp, EM logins) from CUCM via Serviceability API | ||
* **IP Phone webpage**: Collects data (Serial number, CDP/LLDP neighbor info, ITL, Network info) by scraping the IP phone's internal web server | ||
* **Multiple cluster support**: supports integration with multiple CUCM clusters | ||
* **Docker**: Runs easily in Docker containers | ||
|
||
## Screenshots | ||
|
||
### Phone Info Page | ||
![Phone Info](.github/phoneinfo.jpg) | ||
### Phone Scraper Page | ||
![Phone Scraper](.github/phonescraper.jpg) | ||
### Settings Page | ||
![Settings](.github/settings.jpg) | ||
### Job Status Page | ||
![Job Status](.github/jobstatus.jpg) | ||
|
||
## Requirements | ||
|
||
Python 3.7+ | ||
and/or | ||
Docker | ||
|
||
## CUCM Prerequisites | ||
* Create a new application user on your CUCM with the following roles: | ||
* Standard AXL API Access | ||
* Standard CCM Admin Users | ||
* Standard CCMAdmin Read Only | ||
* Standard SERVICEABILITY Read Only | ||
|
||
## Docker Installation | ||
|
||
* Install docker | ||
* clone/download CUCM Phone Info from github | ||
* Edit the Docker environment variables file **docker-fastapi-variables.env** in the project root folder. Create your own secret key and specify your timezone | ||
* run command below from terminal/command line | ||
|
||
<div class="termy"> | ||
|
||
```console | ||
docker-compose up | ||
``` | ||
|
||
</div> | ||
|
||
## First time setup info | ||
|
||
* This will start a HTTPS web server on port 8080 | ||
* Login to https://localhost:8080/ with username 'localadmin' and password 'setup' | ||
* Configure your CUCM clusters on the server. The first CUCM server in the cluster will be used to authenticate future logins to the VOIP Phone Info server. | ||
* Perform manual sync on the Job Status tab | ||
* Set the scheduler to define the automatic update schedule | ||
* All persistent data is stored in the 'data' bind mount. A default self signed certificate is created the first time this application is run. You can replace the files in the 'data/cert' folder with CA signed certificates and then restart the containers. | ||
|
||
## Known issues & limitations | ||
* phone scraper script isn't perfect - some models are not fully working (7940/7960, 7937, Codecs) | ||
* ITL status is populated based on the IP Phone webpage's 'status messages' page. In some circumstances the phone will not have any recently reports of ITL status and therefore the ITL status is not reported. | ||
* testing, has only been tested against CUCM 11.5 | ||
* schema files need to be updated with 12.5 | ||
* no redundancy for CUCM, if the CUCM node in the first cluster is down, logins will fail. | ||
* The CUCM service account credentials are stored using symmetric encryption. You are responsible for limiting access to the 'data' bind mount directory. | ||
|
||
## Third-party software credits | ||
|
||
* This application was built using FastAPI for the backend and VueJS for the front end. This is my first application using both technologies, i'm sure there is room for alot of improvement. | ||
* Handsontable Non-Commercial License - this application is using the Handsontable non-commercial license. This application is for personal use. Please see (https://github.com/handsontable/handsontable/blob/develop/handsontable-non-commercial-license.pdf) for more information. | ||
* Phonescrape library originally from https://github.com/levensailor/phonescrape. Some modifications to parse additional data, support various models, and store results in object | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import os, hashlib | ||
import jwt | ||
import logging | ||
from datetime import datetime, timedelta | ||
from fastapi import HTTPException | ||
|
||
from api.Config import config | ||
|
||
from api.crud import settings_management | ||
|
||
logger = logging.getLogger('api') | ||
|
||
class Auth: | ||
def __init__(self): | ||
self.SECRET_KEY = os.getenv('SECRET_KEY') | ||
self.ALGORITHM = "HS256" | ||
self.ACCESS_TOKEN_EXPIRE_MINUTES = 120 | ||
|
||
|
||
def authenticate_user(self, username, password, cluster=None): # authenticate user against CUCM API | ||
import requests | ||
|
||
if username == "localadmin": | ||
# authenticate to localadmin account | ||
current_hashed_pw = settings_management.get_setting(name='localadmin') | ||
|
||
supplied_hashed_pw = hashlib.sha512((password + str(config.salt)).encode()).hexdigest() | ||
|
||
if supplied_hashed_pw == current_hashed_pw: | ||
return True | ||
else: | ||
return False | ||
else: | ||
# authenticate against CUCM | ||
|
||
|
||
authorized_cucm_users = settings_management.get_all_cucm_users() # get authorized users from DB | ||
|
||
if username in [user_object.userid for user_object in authorized_cucm_users]: | ||
# user is an authorized CUCM user, authenticate user against CUCM UDS interface for Cluster #1 | ||
|
||
cucm_clusters = settings_management.get_cucm_clusters() | ||
|
||
if len(cucm_clusters) > 0: | ||
logger.info(f"SSL verification status {cucm_clusters[0].ssl_verification}") | ||
|
||
url = "https://" + cucm_clusters[0].server + ":8443/cucm-uds/user/" + username | ||
session = requests.Session() | ||
|
||
if cucm_clusters[0].ssl_verification == True and cucm_clusters[0].ssl_ca_trust_file != None: | ||
session.verify = os.path.join(config.ca_certs_folder,cucm_clusters[0].ssl_ca_trust_file) | ||
|
||
if cucm_clusters[0].ssl_verification == False or cucm_clusters[0].ssl_verification == '0': | ||
session.verify = False | ||
|
||
session.auth = (username, password) | ||
try: | ||
response = session.get(url) | ||
except Exception as e: | ||
logger.error(f"Auth failure {e}") | ||
if response.status_code == 200: | ||
return True | ||
|
||
else: | ||
return False | ||
|
||
return False | ||
|
||
def login(self, username, password): | ||
result = self.authenticate_user(username, password) | ||
if result == True: | ||
|
||
expiration = datetime.utcnow() + timedelta(minutes=self.ACCESS_TOKEN_EXPIRE_MINUTES) | ||
|
||
token = jwt.encode({ | ||
'sub': username, | ||
'iat': datetime.utcnow(), | ||
'exp': expiration | ||
}, | ||
self.SECRET_KEY) | ||
|
||
return {"status": "success", "user": username, "token": token.decode('utf-8'), "expiration": str(expiration)} | ||
else: | ||
return {"status": "error", "message": "username/password incorrect"} | ||
|
||
|
||
def validate(self, token): | ||
try: | ||
data = jwt.decode(token, self.SECRET_KEY) | ||
except Exception as e: | ||
if "expired" in str(e): | ||
raise HTTPException(status_code=401, detail={"status": "error", "message": "Token expired"}) | ||
else: | ||
raise HTTPException(status_code=400, detail={"status": "error", "message": "Exception: " + str(e)}) | ||
return data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import os | ||
import logging | ||
from pathlib import Path | ||
from cryptography.fernet import Fernet | ||
from typing import List | ||
from pydantic import BaseModel | ||
|
||
|
||
logger = logging.getLogger('api') | ||
|
||
class ApiConfig: | ||
def __init__(self): | ||
self.basedir = Path(os.path.abspath(__file__)).parents[1] | ||
logger.info(f"Basedir is set to {self.basedir}") | ||
self.datafolder = os.path.join(self.basedir, "data") | ||
self.ca_certs_folder = os.path.join(self.datafolder, "ca_certs") | ||
self.certs_folder = os.path.join(self.datafolder, "certs") | ||
self.database_folder = os.path.join(self.datafolder, "database") | ||
|
||
if not os.path.exists(self.datafolder): | ||
os.mkdir(self.datafolder) | ||
if not os.path.exists(self.ca_certs_folder): | ||
os.mkdir(self.ca_certs_folder) | ||
if not os.path.exists(self.certs_folder): | ||
os.mkdir(self.certs_folder) | ||
if not os.path.exists(self.database_folder): | ||
os.mkdir(self.database_folder) | ||
|
||
if not os.path.exists(os.path.join(self.datafolder, "settings.dat")): | ||
key = Fernet.generate_key() | ||
file = open(os.path.join(self.datafolder, "settings.dat"), 'wb') | ||
file.write(key) # The key is type bytes still | ||
file.close() | ||
|
||
|
||
# settings.dat key | ||
file = open(os.path.join(self.datafolder, "settings.dat"), 'rb') | ||
key = file.read() # The key will be type bytes | ||
self.salt = file.read() | ||
self.key = Fernet(key) | ||
file.close() | ||
|
||
|
||
config = ApiConfig() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import os | ||
import logging | ||
from fastapi import FastAPI, BackgroundTasks | ||
from fastapi.responses import RedirectResponse | ||
from fastapi.staticfiles import StaticFiles | ||
from fastapi.middleware.cors import CORSMiddleware | ||
from fastapi.middleware.gzip import GZipMiddleware | ||
import atexit | ||
|
||
from api.Config import config | ||
from api.db import database | ||
from api.scheduler.Scheduler import scheduler | ||
|
||
api = FastAPI() | ||
api.secret_key = os.getenv('SECRET_KEY') | ||
|
||
# Configure CORS to allow API requests from NPM | ||
origins = [ | ||
"http://127.0.0.1:8081", # allow npm run serve development | ||
] | ||
api.add_middleware( | ||
CORSMiddleware, | ||
allow_origins=origins, | ||
allow_credentials=True, | ||
allow_methods=["*"], | ||
allow_headers=["*"], | ||
) | ||
|
||
api.add_middleware(GZipMiddleware, minimum_size=1000) | ||
|
||
logger = logging.getLogger("api") | ||
|
||
from .routes import auth, phone_data, settings_management | ||
|
||
api.include_router( | ||
auth.router, | ||
prefix='/auth', | ||
tags=["Authentication"] | ||
) | ||
|
||
api.include_router( | ||
phone_data.router, | ||
prefix='/phonedata', | ||
tags=["Phone Data Functions"] | ||
) | ||
|
||
api.include_router( | ||
settings_management.router, | ||
prefix='/settings_management', | ||
tags=["Settings Management Functions"] | ||
) | ||
|
||
api.mount("/home", StaticFiles(directory=os.path.join(config.basedir,"client","dist"), html=True), name="vue-client") | ||
|
||
# redirect root to home static path | ||
@api.get("/") | ||
async def redirect(): | ||
response = RedirectResponse(url='/home/index.html') | ||
return response | ||
|
||
# FAST API Startup tasks | ||
|
||
@api.on_event('startup') | ||
async def startup_event(): | ||
scheduler.start() | ||
|
||
|
||
# FAST API Shutdown tasks | ||
|
||
@api.on_event("shutdown") | ||
async def fastapi_stopped(): | ||
scheduler.shutdown() | ||
|
||
|
||
|
||
|
Empty file.
Empty file.
Oops, something went wrong.