Compare commits

...

6 commits

Author SHA1 Message Date
sepehr
9eec330562 Relase 1.0.8 and Change logs 2025-02-04 13:11:37 +03:00
sepehr
d5059fbb2f The syslog server now utilizes asyncio for improved performance
Enhanced processing of DHCP logs
Added some redis functions
2025-02-03 12:32:09 +03:00
sepehr
ccfe9f622c Added ping data
Device details page now displays current active users
Added ability to terminate active user sessions
Enhanced License Information
2025-02-03 12:29:37 +03:00
sepehr
81a1172660 return false on MikroTik command error 2025-01-28 16:14:04 +03:00
sepehr
d6fbf23067 send task logs to logfile 2025-01-28 16:11:33 +03:00
sepehr
f2b59ea421 Fixes #7 , Stop trying update after 3 failed attempts 2025-01-23 12:03:52 +03:00
11 changed files with 248 additions and 26 deletions

View file

@ -1 +1 @@
__version__ = "1.0.7" __version__ = "1.0.8"

View file

@ -8,12 +8,11 @@
from flask import request,redirect ,session from flask import request,redirect ,session
import datetime import datetime
import html import html
import config import config
import re import re
from libs.red import RedisDB from libs.red import RedisDB
from libs.webutil import app,buildResponse,login_required,get_myself,get_ip,get_agent from libs.webutil import app,buildResponse,login_required,get_myself,get_ip,get_agent
from libs import util from libs import util,ping
from libs.db import db_device,db_groups,db_user_group_perm,db_user_tasks,db_sysconfig,db_syslog from libs.db import db_device,db_groups,db_user_group_perm,db_user_tasks,db_sysconfig,db_syslog
import logging import logging
import json import json
@ -299,12 +298,16 @@ def dev_info():
res=db_device.get_device(devid) res=db_device.get_device(devid)
options=util.build_api_options(db_device.get_devices_by_id([res['id'],])[0]) options=util.build_api_options(db_device.get_devices_by_id([res['id'],])[0])
network_info=[] network_info=[]
res['online']=True
try: try:
if util.check_port(options['host'],options['port']): if util.check_port(options['host'],options['port']):
router=util.RouterOSCheckResource(options) router=util.RouterOSCheckResource(options)
network_info=util.get_network_data(router) network_info=util.get_network_data(router)
del network_info['total'] del network_info['total']
else:
res['online']=False
except: except:
res['online']=False
pass pass
interfaces=[] interfaces=[]
for iface in network_info: for iface in network_info:
@ -325,6 +328,45 @@ def dev_info():
log.error(e) log.error(e)
return buildResponse({'status': 'failed'}, 200, error="Wrong Data") return buildResponse({'status': 'failed'}, 200, error="Wrong Data")
pass pass
try:
res['active_users']=[]
if res['online']:
res['active_users']=tuple(router.api("/user/active/print"))
except:
res['active_users']=[]
try:
res['ping']=ping.get_ping_results(res['ip'], 5, 1)
except Exception as e:
res['ping']=[]
return buildResponse(res,200)
@app.route('/api/dev/kill_session', methods = ['POST'])
@login_required(role='admin',perm={'device':'full'})
def dev_kill_session():
"""return dev info"""
input = request.json
devid=input.get('devid',False)
item=input.get('item',False)
if not devid or not isinstance(devid, int):
return buildResponse({'status': 'failed'},200,error="Wrong Data")
try:
dev=db_device.get_devices_by_id([devid,])[0]
except:
return buildResponse({'status': 'failed'},200,error="Wrong Data")
if not dev:
return buildResponse({'status': 'failed'},200,error="Wrong Data")
options=util.build_api_options(dev)
router=util.RouterOSCheckResource(options)
# active_users=tuple(router.api("/user/active/print"))
# if item in active_users:
try:
acturl=router.api.path("user","active")
res=tuple(acturl('request-logout', **{'.id': item['.id']}))
log.error(res)
except Exception as e:
log.error(e)
pass
res=tuple(router.api("/user/active/print"))
return buildResponse(res,200) return buildResponse(res,200)
@app.route('/api/dev/sensors', methods = ['POST']) @app.route('/api/dev/sensors', methods = ['POST'])

View file

@ -402,6 +402,8 @@ def dashboard_stats():
# res['update_available']=True # res['update_available']=True
if username: if username:
res['username']=username res['username']=username
else:
res['username']=False
res['blog']=[] res['blog']=[]
noconnectiondata={ noconnectiondata={
"content": "Unable to connect to mikrowizard.com! please check server connection", "content": "Unable to connect to mikrowizard.com! please check server connection",

View file

@ -112,7 +112,7 @@ def user_tasks_create():
taskid=task.id taskid=task.id
crontab = CronTab(user=True) crontab = CronTab(user=True)
directory=Path(app.root_path).parent.absolute() directory=Path(app.root_path).parent.absolute()
command = "python3 {}/task_run.py {}".format(directory,taskid) command = "/usr/local/bin/python3 {}/task_run.py {} >> /var/log/cron.log 2>&1".format(directory,taskid)
comment = "MikroWizard task #" + "taskid:{};".format(taskid) comment = "MikroWizard task #" + "taskid:{};".format(taskid)
jobs = crontab.find_comment(comment) jobs = crontab.find_comment(comment)
if len(list(jobs)) > 0: if len(list(jobs)) > 0:
@ -195,8 +195,8 @@ def user_tasks_edit():
crontab.remove(jobs) crontab.remove(jobs)
crontab.write() crontab.write()
job = crontab.new(command=command,comment=comment) job = crontab.new(command=command,comment=comment)
job.setall(cron) job.setall(cron)
crontab.write() crontab.write()
db_syslog.add_syslog_event(get_myself(), "Task","Edit", get_ip(),get_agent(),json.dumps(input)) db_syslog.add_syslog_event(get_myself(), "Task","Edit", get_ip(),get_agent(),json.dumps(input))
return buildResponse([{'status': 'success',"taskid":taskid}],200) return buildResponse([{'status': 'success',"taskid":taskid}],200)
except Exception as e: except Exception as e:

View file

@ -363,6 +363,9 @@ def apply_firmware(packages,firm2,arch,dev,router,events,q):
dev.failed_attempt=dev.failed_attempt+1 dev.failed_attempt=dev.failed_attempt+1
if dev.failed_attempt > 3: if dev.failed_attempt > 3:
db_events.firmware_event(dev.id,"updater","Update Failed","Critical",0,"Unable to Update device") db_events.firmware_event(dev.id,"updater","Update Failed","Critical",0,"Unable to Update device")
dev.save()
q.put({"id": dev.id})
return False
dev.status="updating" dev.status="updating"
dev.save() dev.save()
try: try:

78
py/libs/ping.py Normal file
View file

@ -0,0 +1,78 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# ping.py: ping tool for MikroWizard
# MikroWizard.com , Mikrotik router management solution
# Author: sepehr.ha@gmail.com
import asyncio
import platform
def ping_quality(time_ms):
if time_ms is None:
return "unreachable", "fa-solid fa-times-circle", "#dc3545" # Red, times circle
if time_ms <= 50:
return "excellent", "fa-solid fa-check-circle", "#28a745" # Green, check circle
elif time_ms <= 100:
return "good", "fa-solid fa-thumbs-up", "#80c29e" # Light green, thumbs up
elif time_ms <= 200:
return "average", "fa-solid fa-exclamation-circle", "#ffc107" # Yellow, exclamation circle
else:
return "poor", "fa-solid fa-times-circle", "#dc3545" # Red, times circle
async def ping_host(host, timeout=1):
system = platform.system()
cmd = ["ping", "-c", "1", "-W", str(timeout), host]
process = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
result = stdout.decode().strip()
error = stderr.decode().strip()
# Extract time from output
time_ms = None
if "time=" in result:
try:
time_part = result.split("time=")[-1].split()[0]
time_ms = float(time_part)
except ValueError:
pass
quality, icon, color = ping_quality(time_ms)
raw_response = result.split("\n")[0] if result else error.split("\n")[0]
return {
"host": host,
"status": "success" if time_ms is not None else "failed",
"time": time_ms if time_ms is not None else None,
"ping_quality": quality,
"icon": icon,
"color": color,
"raw_response": raw_response
}
async def multi_ping_one_host(host, count=4, timeout=1):
tasks = [ping_host(host, timeout) for _ in range(count)]
results = await asyncio.gather(*tasks)
successful_pings = [r["time"] for r in results if r["status"] == "success"]
failed_pings = count - len(successful_pings)
average_ping_time = round(sum(successful_pings) / len(successful_pings), 2) if successful_pings else None
response = {
"host": host,
"count": count,
"successful_pings": len(successful_pings),
"failed_pings": failed_pings,
"average_ping_time": average_ping_time,
"results": results
}
return response
def get_ping_results(host, count=4, timeout=1):
return asyncio.run(multi_ping_one_host(host, count, timeout))

View file

@ -36,7 +36,6 @@ class RedisDB(object):
self.r = redis.Redis(host='localhost', port=6379, db=0) self.r = redis.Redis(host='localhost', port=6379, db=0)
self.delta = options.get('delta','') self.delta = options.get('delta','')
def create_sensor_rts(self,sensor): def create_sensor_rts(self,sensor):
retention=self.retention retention=self.retention
if "rx" in sensor or "tx" in sensor: if "rx" in sensor or "tx" in sensor:
@ -137,3 +136,24 @@ class RedisDB(object):
pass pass
return data return data
def store_data(self, device_id, key, command):
"""
store data for specific key of specific command
"""
redis_key = f"device:{device_id}:{key}"
# Add the command to the list
self.r.rpush(redis_key, command.encode('utf-8'))
# Trim the list to keep only the last 20 commands
# self.r.ltrim(redis_key, -20, -1)
def get_last_n_data(self, device_id, key, count=20):
"""
Retrieves the last 'count' data executed for a specific device ID and key.
"""
redis_key = f"device:{device_id}:{key}"
raw_commands = self.r.lrange(redis_key, -count, -1)
return [cmd.decode('utf-8') for cmd in raw_commands]
# return self.r.lrange(redis_key, -count, -1)

View file

@ -416,7 +416,6 @@ def check_syslog_config(dev,router,apply=False):
if len(confs)!=3: if len(confs)!=3:
if apply: if apply:
ids=[item.get('.id') for item in results if 'mikrowizard' in item.get('prefix')] ids=[item.get('.id') for item in results if 'mikrowizard' in item.get('prefix')]
log.error(ids)
if len(ids): if len(ids):
call.remove(*ids) call.remove(*ids)
keys=['critical','error','info'] keys=['critical','error','info']
@ -632,6 +631,8 @@ def run_snippet(dev, snippet):
result=ssh.exec_command(snippet) result=ssh.exec_command(snippet)
if not result: if not result:
result="executed successfully" result="executed successfully"
if "no such item" in result:
result=False
except Exception as e: except Exception as e:
log.error(e) log.error(e)
log_alert('ssh',dev,'During backup ssh error') log_alert('ssh',dev,'During backup ssh error')

View file

@ -17,6 +17,7 @@ from api import api_backups
from api import api_snippet from api import api_snippet
try: try:
from api import api_pro_api from api import api_pro_api
from api import api_pro_api2
except ImportError: except ImportError:
pass pass

View file

@ -5,15 +5,15 @@
# MikroWizard.com , Mikrotik router management solution # MikroWizard.com , Mikrotik router management solution
# Author: sepehr.ha@gmail.com # Author: sepehr.ha@gmail.com
from math import e
import socketserver import socketserver
import re import asyncio
import time import time
import logging
import re
from libs.db import db_device from libs.db import db_device
import logging import logging
from libs.db import db_AA,db_events from libs.db import db_AA,db_events
log = logging.getLogger("SYSLOG")
from libs import util from libs import util
try: try:
from libs import utilpro from libs import utilpro
@ -22,27 +22,40 @@ except ImportError:
ISPRO=False ISPRO=False
pass pass
log = logging.getLogger("SYSLOG")
# A global asyncio event loop
event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(event_loop)
class SyslogUDPHandler(socketserver.BaseRequestHandler): class SyslogUDPHandler(socketserver.BaseRequestHandler):
def extract_data_from_regex(self,regex,line): def extract_data_from_regex(self, regex, line):
try: try:
matches = re.finditer(regex, line, re.MULTILINE) matches = re.finditer(regex, line, re.MULTILINE)
sgroups=[] sgroups = []
for matchNum, match in enumerate(matches, start=1): for matchNum, match in enumerate(matches, start=1):
for groupNum in range(0, len(match.groups())): for groupNum in range(0, len(match.groups())):
groupNum = groupNum + 1 groupNum = groupNum + 1
sgroups.append(match.group(groupNum)) sgroups.append(match.group(groupNum))
return sgroups return sgroups
except: except Exception as e:
log.error(f"Regex error: {e}")
return None return None
def handle(self): def handle(self):
# Run the coroutine in the global event loop
asyncio.run_coroutine_threadsafe(self.handle_log(), event_loop)
# Respond to the client (optional)
async def handle_log(self):
data = bytes.decode(self.request[0].strip(), encoding="utf-8") data = bytes.decode(self.request[0].strip(), encoding="utf-8")
message = str(data) message = str(data)
#get current timestamp #get current timestamp
ts = int(time.time()) ts = int(time.time())
socket = self.request[1] socket = self.request[1]
dev=db_device.query_device_by_ip(self.client_address[0]) dev=db_device.query_device_by_ip(self.client_address[0])
regex=r'(.*),?(info.*|warning|critical) mikrowizard(\d+):.*' regex=r'(.*),?(info.*|warning|critical|error) mikrowizard(\d+):.*'
if dev: if dev:
info=self.extract_data_from_regex(regex,message) info=self.extract_data_from_regex(regex,message)
opts=util.build_api_options(dev) opts=util.build_api_options(dev)
@ -53,6 +66,7 @@ class SyslogUDPHandler(socketserver.BaseRequestHandler):
except: except:
log.error("**device id mismatch") log.error("**device id mismatch")
log.error(message) log.error(message)
log.error(info)
log.error(self.client_address[0]) log.error(self.client_address[0])
log.error("device id mismatch**") log.error("device id mismatch**")
dev=False dev=False
@ -96,6 +110,9 @@ class SyslogUDPHandler(socketserver.BaseRequestHandler):
regex= r"system,info mikrowizard\d+: (.*) (changed|added|removed|unscheduled) by (winbox-\d.{1,3}\d\/.*\(winbox\)|mac-msg\(winbox\)|tcp-msg\(winbox\)|ssh|telnet|api|api-ssl|.*\/web|ftp|www-ssl).*:(.*)@(.*) \((.*)\)" regex= r"system,info mikrowizard\d+: (.*) (changed|added|removed|unscheduled) by (winbox-\d.{1,3}\d\/.*\(winbox\)|mac-msg\(winbox\)|tcp-msg\(winbox\)|ssh|telnet|api|api-ssl|.*\/web|ftp|www-ssl).*:(.*)@(.*) \((.*)\)"
#with new versions of mikrotik syslog is not sending the correct trace in message #with new versions of mikrotik syslog is not sending the correct trace in message
buged_regex=r"system,info mikrowizard\d+: (.*) (changed|added|removed|unscheduled) by \((.*)\)" buged_regex=r"system,info mikrowizard\d+: (.*) (changed|added|removed|unscheduled) by \((.*)\)"
if ISPRO:
# threading.Thread(target=utilpro.do_pro,args=()).start()
utilpro.do_pro("syslog", False, dev, message)
if re.match(regex, message): if re.match(regex, message):
info=self.extract_data_from_regex(regex, message) info=self.extract_data_from_regex(regex, message)
address=info[4].split('/') address=info[4].split('/')
@ -138,15 +155,38 @@ class SyslogUDPHandler(socketserver.BaseRequestHandler):
elif "link up" in message: elif "link up" in message:
info=self.extract_data_from_regex(link_regex,message) info=self.extract_data_from_regex(link_regex,message)
util.check_or_fix_event(events,'state',"Link Down: " + info[0]) util.check_or_fix_event(events,'state',"Link Down: " + info[0])
elif "dhcp,info mikrowizard" in message: elif any(term in message for term in ["dhcp,info","dhcp,critical","dhcp,warning"]):
dhcp_regex=r'dhcp,info mikrowizard\d+: (dhcp-client|.*) (deassigned|assigned|.*) (\d+\.\d+\.\d+\.\d+|on.*address)\s*(from|to|$)\s*(.*)' type='cleint'
# if (" dhcp-client on" in message):
# dhcp_regex=r'dhcp,info mikrowizard\d+: dhcp-client on (.*) (got IP address|lost IP address) (\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b) ?-? ?(.*)?'
# else:
# dhcp_regex=r'dhcp,info mikrowizard\d+: (.*) (assigned|deassigned) (\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[A-Fa-f0-9]{1,4}:){7}[A-Fa-f0-9]{1,4}\b) (for|to|from) (\b([A-Fa-f0-9]{2}[:-]){5}[A-Fa-f0-9]{2}\b)? ?(.*)?'
# type='server'
if (not " dhcp-client on" in message):
type='server'
# dhcp_regex=r'dhcp,info mikrowizard\d+: (dhcp-client|.*) (deassigned|assigned|.*) (\d+\.\d+\.\d+\.\d+|on.*address)\s*(from|to|for|- lease stopped locally|$)\s*(.*)'
dhcp_regex=r'dhcp,(?:info|warning|critical|error)(?:,info|,warning|,critical|,error)? mikrowizard\d+: (.*)'
info=self.extract_data_from_regex(dhcp_regex,message) info=self.extract_data_from_regex(dhcp_regex,message)
if info and "assigned" in message: if "dhcp,info" in message:
db_events.state_event(dev.id, "syslog", "dhcp assigned","info",1,"server {} assigned {} to {}".format(info[0],info[2],info[4])) level="info"
elif info and "deassigned" in message: elif "dhcp,warning" in message:
db_events.state_event(dev.id, "syslog", "dhcp deassigned","info",1,"server {} deassigned {} from {}".format(info[0],info[2],info[4])) level="warning"
elif info and "dhcp-client" in message: elif "dhcp,critical" in message:
db_events.state_event(dev.id, "syslog", "dhcp client","info",1,"{} {}".format(info[1],info[2])) level="critical"
else:
level="error"
if type=='server':
if info and "deassigned" in message:
log.error("Logging deassigned")
db_events.state_event(dev.id, "syslog", "dhcp deassigned",level,1,"{}".format(info[0]))
# db_events.state_event(dev.id, "syslog", "dhcp assigned","info",1,"server {} assigned {} to {}".format(info[0],info[2],info[4]))
elif info and "assigned" in message:
log.error("Logging deassigned")
db_events.state_event(dev.id, "syslog", "dhcp assigned",level,1,"{}".format(info[0]))
# db_events.state_event(dev.id, "syslog", "dhcp deassigned","info",1,"server {} deassigned {} from {}".format(info[0],info[2],info[4]))
else:
db_events.state_event(dev.id, "syslog", "dhcp client",level,1,"{}".format(info[0]))
# db_events.state_event(dev.id, "syslog", "dhcp client","info",1,"{} {}".format(info[1],info[2]))
elif "wireless,info mikrowizard" in message: elif "wireless,info mikrowizard" in message:
if ISPRO: if ISPRO:
utilpro.wireless_syslog_event(dev ,message) utilpro.wireless_syslog_event(dev ,message)
@ -162,9 +202,24 @@ class SyslogUDPHandler(socketserver.BaseRequestHandler):
log.error(message) log.error(message)
else: else:
log.error(message) log.error(message)
def start_event_loop(loop):
"""Run the event loop in a separate thread."""
asyncio.set_event_loop(loop)
loop.run_forever()
if __name__ == "__main__": if __name__ == "__main__":
try: try:
server = socketserver.UDPServer(("0.0.0.0",5014), SyslogUDPHandler) # Start the asyncio event loop in a separate thread
server.serve_forever(poll_interval=0.5) import threading
thread = threading.Thread(target=start_event_loop, args=(event_loop,), daemon=True)
thread.start()
# Start the UDP server
server = socketserver.UDPServer(("0.0.0.0", 5014), SyslogUDPHandler)
server.serve_forever()
except (IOError, SystemExit): except (IOError, SystemExit):
raise raise
except KeyboardInterrupt:
log.info("Shutting down server")
event_loop.stop()

View file

@ -1,5 +1,25 @@
# Release Notes # Release Notes
## Version 1.0.8 Free / 1.1.0 Pro
### New Features
- Router Ping Information: Added ping data to enhance connectivity monitoring.
- Active User Sessions: Device details page now displays current active users.
- Session Management: Introduced the ability to terminate active user sessions.
- Enhanced License Information: Dashboard now provides more detailed license-related insights.
- MikroTik Configuration Sync & Config Cloner (Pro): Introduced a new menu/page for configuration cloning/sync.
- DHCP Server & Lease History (Pro): DHCP server details along with historical lease information in device details.
### Improvements & Bug Fixes
- Async Syslog Server: The syslog server now utilizes asyncio for improved performance and efficiency.
- DHCP Log Handling: Enhanced processing of DHCP logs in the syslog system.
- Firmware Updater Fix: Resolved an issue where the firmware updater failed to retry properly.
---
## Version 1.0.7 - Fast update
- Firmware updater fix: Fix broken frimware update
## Version 1.0.6 - Firmware upgrade fix ## Version 1.0.6 - Firmware upgrade fix
### Bugs Fixed ### Bugs Fixed