MikroWizard.mikroman/py/api/api_dev.py
sepehr 70dc0ddc55 Bugs:
Fixed Firmware download from the Mikrotik website when there are multiple npk available
Fixed Mikrowizard system permission error when it is set to None
Fixed user device group permissions
Some minor UI improvements
Fix IP scan for one IP scan / Fix not scanning the last IP in the range
Fix manual snippet execution not working when device groups are selected
Some minor bug fixes and improvements

New:
Show background tasks and be able to stop them while running in the background (like an IP scanner)
Add support for manual MikroWizard update dashboard/settings page
update to version 1.0.5

Enhancement:
Show permission error in some pages when the  user doesn't have permission for that page/action
show better charts/graphs in the dashboard and device interface details
show more info on the dashboard about update and version information and license
2025-01-02 20:12:00 +03:00

566 lines
22 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
# api_bakcups.py: API for managing bakcups
# MikroWizard.com , Mikrotik router management solution
# Author: sepehr.ha@gmail.com
from flask import request,redirect ,session
import datetime
import html
import config
import re
from libs.red import RedisDB
from libs.webutil import app,buildResponse,login_required,get_myself,get_ip,get_agent
from libs import util
from libs.db import db_device,db_groups,db_user_group_perm,db_user_tasks,db_sysconfig,db_syslog
import logging
import json
from playhouse.shortcuts import model_to_dict
log = logging.getLogger("api")
try:
from libs import utilpro
ISPRO=True
except ImportError:
ISPRO=False
pass
@app.route('/', methods = ['GET'])
def index():
"""Just a redirect to api list."""
if config.IS_PRODUCTION:
return "not available", 400
return redirect('/api/list')
@app.route('/api/dev/list', methods = ['POST'])
@login_required(role='admin',perm={'device':'read'})
def list_devs():
"""Return devs list of assigned to user , all for admin"""
input = request.json
# Get devices that are in the group
group_id = int(input.get('group_id', False))
page = input.get('page')
size = input.get('size')
search = input.get('search',False)
page = int(page or 0)
limit = int(size or 1000)
res = []
try:
# Get devices that current user have access
uid = session.get("userid") or False
if not uid:
return buildResponse({'result':'failed','err':"No User"}, 200)
# Get devices that current user have access
devs=db_user_group_perm.DevUserGroupPermRel.get_user_devices(uid,group_id).paginate(page, limit).dicts()
for dev in devs:
temp=dev
del temp['user_name']
del temp['password']
if ' ' not in temp['uptime']:
temp['uptime'] = temp['uptime'].replace('w',' week ').replace('d',' day ').replace('h',' hour ').replace('m',' min ')
res.append(temp)
except Exception as e:
return buildResponse({'result':'failed','err':str(e)},200)
return buildResponse(res,200)
@app.route('/api/dev/get_editform', methods = ['POST'])
@login_required(role='admin',perm={'device':'full'})
def get_editform():
"""return device editable data"""
input = request.json
# get devices that are in the group
devid = int(input.get('devid', False))
res = {}
try:
dev=db_device.get_device(devid)
if not dev:
return buildResponse({'status': 'failed'}, 200, error="Wrong Data")
res['user_name']=util.decrypt_data(dev['user_name'])
if ISPRO:
res['password']="Password is Hidden"
else:
res['password']=util.decrypt_data(dev['password'])
res['ip']=dev['ip']
res['peer_ip']=dev['peer_ip']
res['name']=dev['name']
res['id']=dev['id']
try:
res['ips']=json.loads(db_sysconfig.get_sysconfig('all_ip'))
except Exception as e:
res['ips']=[]
except Exception as e:
log.error(e)
return buildResponse({'status': 'failed'}, 200, error="Wrong Data")
return buildResponse(res,200)
@app.route('/api/dev/save_editform', methods = ['POST'])
@login_required(role='admin', perm={'device':'full'})
def save_editform():
"""save device configuration"""
input = request.json
devid = int(input.get('id', False))
user_name = input.get('user_name', False)
password = input.get('password', False)
ip = input.get('ip', False)
peer_ip = input.get('peer_ip', False)
name = input.get('name', False)
try:
if db_device.update_device(devid, util.crypt_data(user_name), util.crypt_data(password), ip, peer_ip, name):
db_syslog.add_syslog_event(get_myself(), "Device", "Edit", get_ip(),get_agent(),json.dumps(input))
return buildResponse({"result":"success"}, 200)
else:
return buildResponse({"result":"failed","err":"Unable to update device"}, 200)
except Exception as e:
log.error(e)
return buildResponse({"result":"failed","err":str(e)}, 200)
@app.route('/api/devgroup/list', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'read'})
def list_devgroups():
"""return dev groups"""
# build HTML of the method list
devs = []
uid=session.get("userid") or False
try:
perms=list(db_user_group_perm.DevUserGroupPermRel.get_user_group_perms(uid))
group_ids = [perm.group_id for perm in perms]
if str(uid) == "37cc36e0-afec-4545-9219-94655805868b":
group_ids=False
devs=list(db_groups.query_groups_api(group_ids))
except Exception as e:
return buildResponse({'result':'failed','err':str(e)},200)
return buildResponse(devs,200)
@app.route('/api/devgroup/delete', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'full'})
def delete_group():
"""delete dev group"""
input = request.json
gid = input.get('gid', False)
try:
if db_user_group_perm.DevUserGroupPermRel.delete_group(gid):
db_syslog.add_syslog_event(get_myself(), "Device Group","Delete", get_ip(),get_agent(),json.dumps(input))
return buildResponse({"result":"success"}, 200)
else:
return buildResponse({"result":"failed",'err':'Unable to delete'}, 200)
except Exception as e:
return buildResponse({"result":"failed",'err':'Unable to delete'}, 200)
@app.route('/api/devgroup/members', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'read','device':'read'})
def list_devgroups_members():
"""return list of dev groups"""
input = request.json
gid=input.get('gid',False)
# get devices that are in the group
devs = []
try:
devs=list(db_groups.devs(gid))
except Exception as e:
return buildResponse({'result':'failed','err':str(e)},200)
return buildResponse(devs,200)
@app.route('/api/devgroup/update_save_group', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'write','device':'read'})
def update_save_group():
"""save device group config"""
input = request.json
devids= input.get('array_agg', False)
name = input.get('name', False)
id = input.get('id', False)
# First check if we are editiong or creating new group
# if id is 0 then we are creating new group
if id==0:
# create new group and add devices to it
try:
group=db_groups.create_group(name)
if group:
db_syslog.add_syslog_event(get_myself(), "Device Group","Create", get_ip(),get_agent(),json.dumps(input))
gid=group.id
db_groups.add_devices_to_group(gid,devids)
else:
return buildResponse({'result':'failed','err':"Group not created"}, 200)
return buildResponse({"result":"success"}, 200)
except Exception as e:
return buildResponse({'result':'failed','err':str(e)}, 200)
else:
# update group and add devices to it
try:
group=db_groups.update_group(id, name)
db_groups.add_devices_to_group(group.id, devids)
#get all dev ids from group and compare to devids,remove devs not availble in devids
devs=db_groups.devs2(id)
ids=[]
for dev in devs:
ids.append(dev.id)
dev_to_remove=list(set(ids)-set(devids))
db_groups.delete_from_group(dev_to_remove)
db_syslog.add_syslog_event(get_myself(), "Device Group","Update", get_ip(),get_agent(),json.dumps(input))
return buildResponse({"result":"success"}, 200)
except Exception as e:
return buildResponse({'result':'failed','err':str(e)}, 200)
@app.route('/api/search/groups', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'read','device':'read'})
def search_groups():
"""search in devices"""
input = request.json
searchstr=input.get('searchstr',False)
dev_groups = []
group=db_groups.DevGroups
try:
if searchstr and searchstr!="":
# find device groups that contains searchstr in the name
dev_groups = (group
.select()
.where(group.name.contains(searchstr))
.dicts())
else:
# return first 10 ordered alphabeticaly
dev_groups = (group
.select()
.order_by(group.name)
.limit(10)
.dicts())
except Exception as e:
return buildResponse({'result':'failed','err':str(e)},200)
return buildResponse(dev_groups,200)
@app.route('/api/search/devices', methods = ['POST'])
@login_required(role='admin',perm={'device':'read'})
def search_devices():
"""search in groups"""
input = request.json
searchstr=input.get('searchstr',False)
# build HTML of the method list
device=db_device.Devices
searchstr=input.get('searchstr',False)
devs = []
try:
if searchstr and searchstr!="":
# find devices that contains searchstr in the name
devs = (device
.select()
.where(device.name.contains(searchstr))
.dicts())
else:
# return first 10 ordered alphabeticaly
devs = (device
.select()
.order_by(device.name)
.limit(10)
.dicts())
except Exception as e:
return buildResponse({'result':'failed','err':str(e)},200)
return buildResponse(devs,200)
@app.route('/api/taskmember/details', methods = ['POST'])
@login_required(role='admin',perm={'device_group':'read','device':'read'})
def get_taskmember_details():
"""search in groups"""
# build HTML of the method list
input = request.json
tid=input.get('taskid',False)
if not tid:
return buildResponse({"success":'failed',"err":"Wrong task"},200)
res=[]
utask=db_user_tasks.UserTasks.get_utask_by_id(tid)
members=db_user_tasks.get_task_devices(utask,False)
if utask.selection_type=="groups":
for group in members:
tmp = model_to_dict(group)
res.append({"id":tmp['id'], "name":tmp['name']})
else:
for dev in members:
tmp = model_to_dict(dev)
res.append({"id":tmp['id'],"name":tmp['name'],"mac":tmp['mac']})
return buildResponse(res,200)
@app.route('/api/dev/info', methods = ['POST'])
@login_required(role='admin',perm={'device':'read'})
def dev_info():
"""return dev info"""
input = request.json
devid=input.get('devid',False)
if not devid or not isinstance(devid, int):
return buildResponse({'status': 'failed'},200,error="Wrong Data")
res=db_device.get_device(devid)
options=util.build_api_options(db_device.get_devices_by_id([res['id'],])[0])
network_info=[]
try:
if util.check_port(options['host'],options['port']):
router=util.RouterOSCheckResource(options)
network_info=util.get_network_data(router)
del network_info['total']
except:
pass
interfaces=[]
for iface in network_info:
interfaces.append(network_info[iface])
#fix and change some data
res['interfaces']=interfaces
res.pop('user_name')
res.pop('password')
res.pop('wifi_config')
res['created']=res['created'].strftime("%Y-%m-%d %H:%M:%S")
res['modified']=res['modified'].strftime("%Y-%m-%d %H:%M:%S")
#get data from redis
if ISPRO:
res['is_radio']=utilpro.check_is_radio(res['id'])
try:
del res['sensors']
except Exception as e:
log.error(e)
return buildResponse({'status': 'failed'}, 200, error="Wrong Data")
pass
return buildResponse(res,200)
@app.route('/api/dev/sensors', methods = ['POST'])
@login_required(role='admin',perm={'device':'read'})
def dev_sensors():
"""return dev sensors chart data"""
input = request.json
devid=input.get('devid',False)
total=input.get('total','bps')
delta=input.get('delta',"5m")
if delta not in ["5m","1h","daily","live"]:
return buildResponse({'status': 'failed'},200,error="Wrong Data")
if not devid or not isinstance(devid, int):
return buildResponse({'status': 'failed'},200,error="Wrong Data")
dev=db_device.get_device(devid)
if delta=="5m":
start_time=datetime.datetime.now()-datetime.timedelta(minutes=5*24)
elif delta=="1h":
start_time=datetime.datetime.now()-datetime.timedelta(hours=24)
elif delta=="daily":
start_time=datetime.datetime.now()-datetime.timedelta(days=30)
else:
start_time=datetime.datetime.now()-datetime.timedelta(days=30)
end_time=datetime.datetime.now()
try:
res={}
res['sensors']=json.loads(dev['sensors'])
redopts={
"dev_id":dev['id'],
"keys":res['sensors'],
"start_time":start_time,
"end_time":end_time,
"delta":delta,
}
colors={
'backgroundColor': 'rgba(77,189,116,.2)',
'borderColor': '#4dbd74',
'pointHoverBackgroundColor': '#fff'
}
reddb=RedisDB(redopts)
data=reddb.get_dev_data_keys()
tz=db_sysconfig.get_sysconfig('timezone')
res["radio-sensors"]=[]
for key in res['sensors'][:]:
if "rx" in key or "tx" in key or "rxp" in key or "txp" in key or "radio" in key:
if "radio" in key:
res["radio-sensors"].append(key)
if not 'total' in key:
res['sensors'].remove(key)
continue
if "total" in key:
if total=='bps' and 'rx/tx-total' in res['sensors'] and 'rx/tx-total' in res['sensors']:
continue
if total!='bps' and 'rxp/txp-total' in res['sensors'] and 'rxp/txp-total' in res['sensors']:
continue
temp=[]
ids=['yA','yB']
colors=['#17522f','#171951']
datasets=[]
lables=[]
data_keys=['tx-total','rx-total']
if total!='bps':
data_keys=['txp-total','rxp-total']
for idx, val in enumerate(data_keys) :
for d in data[val]:
if len(lables) <= len(data[val]):
edatetime=datetime.datetime.fromtimestamp(d[0]/1000)
lables.append(util.utc2local(edatetime,tz=tz).strftime("%m/%d/%Y, %H:%M:%S %Z"))
temp.append(round(d[1],1))
datasets.append({'borderColor': colors[idx],'type': 'line','yAxisID': ids[idx],'data':temp,'unit':val.split("-")[0],'backgroundColor': colors[idx],'pointHoverBackgroundColor': '#fff'})
temp=[]
if total=='bps':
res["rx/tx-total"]={'labels':lables,'datasets':datasets}
res['sensors'].append("rx/tx-total")
else:
res["rxp/txp-total"]={'labels':lables,'datasets':datasets}
res['sensors'].append("rxp/txp-total")
else:
temp={"labels":[],"data":[]}
for d in data[key]:
edatetime=datetime.datetime.fromtimestamp(d[0]/1000)
temp["labels"].append(util.utc2local(edatetime,tz=tz).strftime("%m/%d/%Y, %H:%M:%S %Z"))
temp["data"].append(round(d[1],1))
res[key]={'labels':temp["labels"],'datasets':[{'data':temp['data'],'backgroundColor': 'rgba(77,189,116,.2)','borderColor': '#fff','pointHoverBackgroundColor': '#fff'}]}
if 'rxp-total' in res['sensors']:
res['sensors'].remove('txp-total')
res['sensors'].remove('rxp-total')
elif 'rx-total' in res['sensors']:
res['sensors'].remove('tx-total')
res['sensors'].remove('rx-total')
except Exception as e:
log.error(e)
return buildResponse({'status': 'failed'}, 200, error="Error in generating data")
pass
return buildResponse(res,200)
@app.route('/api/dev/ifstat', methods = ['POST'])
@login_required(role='admin',perm={'device':'read'})
def dev_ifstat():
"""return device interfaces info"""
input = request.json
devid=input.get('devid',False)
chart_type=input.get('type','bps')
delta=input.get('delta',"5m")
interface=input.get('interface',False)
if delta not in ["5m","1h","daily","live"]:
return buildResponse({'status': 'failed'},200,error="Wrong Data")
if not devid or not isinstance(devid, int):
return buildResponse({'status': 'failed'},200,error="Wrong Data")
res=db_device.get_device(devid)
if delta=="5m":
start_time=datetime.datetime.now()-datetime.timedelta(minutes=5*24)
elif delta=="1h":
start_time=datetime.datetime.now()-datetime.timedelta(hours=24)
elif delta=="daily":
start_time=datetime.datetime.now()-datetime.timedelta(days=30)
else:
start_time=datetime.datetime.now()-datetime.timedelta(days=30)
end_time=datetime.datetime.now()
#Fix and change some data
#Get data from redis
res['name']="Device : " + db_device.get_device(devid)['name'] + " - Interface : " + interface
try:
res['sensors']=json.loads(res['sensors'])
for sensor in res['sensors'][:]:
regex=r'.*{}$'.format(interface)
if not bool(re.match(regex,sensor)):
res['sensors'].remove(sensor)
redopts={
"dev_id":res['id'],
"keys":res['sensors'],
"start_time":start_time,
"end_time":end_time,
"delta":delta,
}
colors={
'backgroundColor': 'rgba(77,189,116,.2)',
'borderColor': '#4dbd74',
'pointHoverBackgroundColor': '#fff'
}
reddb=RedisDB(redopts)
data=reddb.get_dev_data_keys()
temp=[]
ids=['yA','yB']
colors=['#4caf50','#ff9800']
bgcolor=['rgba(76, 175, 80, 0.2)','rgba(255, 152, 0, 0.2)']
datasets=[]
lables=[]
tz=db_sysconfig.get_sysconfig('timezone')
data_keys=['tx-{}'.format(interface),'rx-{}'.format(interface)]
if chart_type=='bps':
data_keys=['tx-{}'.format(interface),'rx-{}'.format(interface)]
elif chart_type=='pps':
data_keys=['txp-{}'.format(interface),'rxp-{}'.format(interface)]
for idx, val in enumerate(data_keys):
for d in data[val]:
if len(lables) <= len(data[val]):
edatetime=datetime.datetime.fromtimestamp(d[0]/1000)
lables.append(util.utc2local(edatetime,tz=tz).strftime("%Y-%m-%d %H:%M:%S"))
temp.append(round(d[1],1))
datasets.append({'label':val,'borderColor': colors[idx],'type': 'line','yAxisID': ids[idx],'data':temp,'unit':val.split("-")[0],'backgroundColor': bgcolor[idx],'pointHoverBackgroundColor': '#fff','fill': True})
temp=[]
res["data"]={'labels':lables,'datasets':datasets}
except Exception as e:
log.error(e)
return buildResponse({'status': 'failed'}, 200, error="Error in generating data")
pass
return buildResponse(res,200)
@app.route('/api/dev/delete', methods = ['POST'])
@login_required(role='admin',perm={'device':'full'})
def dev_delete():
"""return dev info"""
input = request.json
devids=input.get('devids', False)
res={}
# ToDo: we need to delete redis keys also
try:
for dev in devids:
if db_groups.delete_device(dev):
db_syslog.add_syslog_event(get_myself(), "Device","Delete", get_ip(),get_agent(),json.dumps(input))
res['status']='success'
else:
res['status'] = 'failed'
res['err'] = 'Unable to Delete Device'
except Exception as e:
log.error(e)
return buildResponse({'status': 'failed'}, 200, error=str(e))
return buildResponse(res, 200)
#Development tool , We dont want this in production
@app.route('/api/list', methods = ['GET'])
def list_api():
"""List the available REST APIs in this service as HTML. Queries
methods directly from Flask, no need to maintain separate API doc.
(Maybe this could be used as a start to generate Swagger API spec too.)"""
# decide whether available in production
if config.IS_PRODUCTION:
return "not available in production", 400
# build HTML of the method list
apilist = []
rules = sorted(app.url_map.iter_rules(), key=lambda x: str(x))
for rule in rules:
f = app.view_functions[rule.endpoint]
docs = f.__doc__ or ''
module = f.__module__ + ".py"
# remove noisy OPTIONS
methods = sorted([x for x in rule.methods if x != "OPTIONS"])
url = html.escape(str(rule))
if not "/api/" in url and not "/auth/" in url:
continue
apilist.append("<div><a href='{}'><b>{}</b></a> {}<br/>{} <i>{}</i></div>".format(
url, url, methods, docs, module))
header = """<body>
<title>MikroWizard Generated API LIST</title>
<style>
body { width: 80%; margin: 20px auto;
font-family: Courier; }
section { background: #eee; padding: 40px 20px;
border: 1px dashed #aaa; }
i { color: #888; }
</style>"""
title = """
<section>
<h2>REST API ({} end-points)</h2>
<h3>IS_PRODUCTION={} IS_LOCAL_DEV={} Started ago={}</h3>
""".format(len(apilist), config.IS_PRODUCTION, config.IS_LOCAL_DEV,
config.started_ago(True))
footer = "</section></body>"
return header + title + "<br/>".join(apilist) + footer