Initial YakPanel commit
This commit is contained in:
0
mod/project/ssh/__init__.py
Normal file
0
mod/project/ssh/__init__.py
Normal file
159
mod/project/ssh/base.py
Normal file
159
mod/project/ssh/base.py
Normal file
@@ -0,0 +1,159 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
import public
|
||||
|
||||
|
||||
class SSHbase:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def return_area(result, key):
|
||||
"""
|
||||
@name 格式化返回带IP归属地的数组
|
||||
@param result<list> 数据数组
|
||||
@param key<str> ip所在字段
|
||||
@return list
|
||||
"""
|
||||
if not result:
|
||||
return result
|
||||
|
||||
# 添加IP查询缓存
|
||||
ip_cache_file = 'data/ip_location_cache.json'
|
||||
ip_cache = {}
|
||||
|
||||
# 确保缓存目录存在
|
||||
cache_dir = os.path.dirname(ip_cache_file)
|
||||
if not os.path.exists(cache_dir):
|
||||
os.makedirs(cache_dir, exist_ok=True)
|
||||
|
||||
# 读取IP缓存
|
||||
try:
|
||||
if os.path.exists(ip_cache_file):
|
||||
ip_cache = json.loads(public.readFile(ip_cache_file))
|
||||
except Exception as e:
|
||||
public.print_log('Failed to read IP cache: {}'.format(str(e)))
|
||||
ip_cache = {}
|
||||
|
||||
# 只查询未缓存的IP
|
||||
new_ips = set()
|
||||
for data in result:
|
||||
ip = data.get(key)
|
||||
if not ip or public.is_ipv6(ip):
|
||||
continue
|
||||
if ip not in ip_cache:
|
||||
new_ips.add(ip)
|
||||
|
||||
# 批量查询新IP
|
||||
for ip in new_ips:
|
||||
try:
|
||||
if "127.0.0" in ip:
|
||||
ip_cache[ip] = {"info": "Local address (e.g. left terminal)"}
|
||||
continue
|
||||
|
||||
ip_area = public.get_ip_location(ip)
|
||||
if not ip_area:
|
||||
ip_cache[ip] = {"info": "unknown area"}
|
||||
continue
|
||||
|
||||
ip_area = ip_area.raw
|
||||
country = ip_area.get("country", {})
|
||||
ip_area["info"] = "{} {} {}".format(
|
||||
country.get('country', 'unknown'),
|
||||
country.get('province', 'unknown'),
|
||||
country.get('city', 'unknown')
|
||||
) if country else "unknown area"
|
||||
ip_cache[ip] = ip_area
|
||||
except Exception as e:
|
||||
public.print_log('Query IP {} Failed: {}'.format(ip, str(e)))
|
||||
ip_cache[ip] = {"info": "unknown area"}
|
||||
|
||||
# 只有当有新IP被查询时才更新缓存文件
|
||||
if new_ips:
|
||||
try:
|
||||
public.writeFile(ip_cache_file, json.dumps(ip_cache))
|
||||
except Exception as e:
|
||||
public.print_log('Failed to update IP cache: {}'.format(str(e)))
|
||||
pass
|
||||
|
||||
# 使用缓存数据,确保不修改原始数据
|
||||
result_with_area = []
|
||||
for data in result:
|
||||
data_copy = data.copy() # 创建数据副本
|
||||
ip = data_copy.get(key, '')
|
||||
data_copy['area'] = ip_cache.get(ip, {"info": "unknown area"})
|
||||
result_with_area.append(data_copy)
|
||||
|
||||
return result_with_area
|
||||
|
||||
@staticmethod
|
||||
def journalctl_system():
|
||||
try:
|
||||
if os.path.exists('/etc/os-release'):
|
||||
f = public.readFile('/etc/os-release')
|
||||
f = f.split('\n')
|
||||
ID = ''
|
||||
VERSION_ID = 0
|
||||
for line in f:
|
||||
if line.startswith('VERSION_ID'):
|
||||
VERSION_ID = int(line.split('=')[1].split('.')[0].strip('"'))
|
||||
if line.startswith('ID'):
|
||||
if ID != '': continue
|
||||
ID = line.strip().split('=')[1].strip('"')
|
||||
try:
|
||||
ID = ID.split('.')[0]
|
||||
except:
|
||||
pass
|
||||
if (ID.lower() == 'debian' and VERSION_ID >= 11) or (ID.lower() == 'ubuntu' and VERSION_ID >= 20):
|
||||
return True
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def parse_login_entry(parts, year):
|
||||
"""解析登录条目"""
|
||||
try:
|
||||
# 判断日志格式类型
|
||||
if 'T' in parts[0]: # centos7以外的格式
|
||||
# 解析ISO格式时间戳
|
||||
dt = datetime.fromisoformat(parts[0].replace('Z', '+00:00'))
|
||||
user_index = parts.index('user') + 1 if 'user' in parts else parts.index('for') + 1
|
||||
ip_index = parts.index('from') + 1
|
||||
port_index = parts.index('port') + 1 if 'port' in parts else -1
|
||||
else:
|
||||
# 解析传统格式时间
|
||||
month = parts[0]
|
||||
day = parts[1]
|
||||
time_str = parts[2]
|
||||
# 如果月份大于当前月,说明年份不对,直接把year修改成1970年
|
||||
if datetime.strptime("{} {}".format(month, day), "%b %d").month > datetime.now().month:
|
||||
year = "1970"
|
||||
dt_str = "{} {} {} {}".format(month, day, year, time_str)
|
||||
dt = datetime.strptime(dt_str, "%b %d %Y %H:%M:%S")
|
||||
user_index = parts.index('for') + 1 if "invalid" not in parts else -6
|
||||
ip_index = parts.index('from') + 1
|
||||
port_index = parts.index('port') + 1 if 'port' in parts else -1
|
||||
|
||||
entry = {
|
||||
"timestamp": int(dt.timestamp()),
|
||||
"time": dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"type": "success" if ("Accepted" in parts) else "failed",
|
||||
"status": 1 if ("Accepted" in parts) else 0,
|
||||
"user": parts[user_index],
|
||||
"address": parts[ip_index],
|
||||
"port": parts[port_index] if port_index != -1 else "",
|
||||
"deny_status": 0,
|
||||
"login_type": "publickey" if "publickey" in parts else "password" # 添加登录类型
|
||||
}
|
||||
return entry
|
||||
except Exception as e:
|
||||
public.print_log(public.get_error_info())
|
||||
return None
|
||||
231
mod/project/ssh/comMod.py
Normal file
231
mod/project/ssh/comMod.py
Normal file
@@ -0,0 +1,231 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
import public
|
||||
# from mod.project.ssh.base import SSHbase
|
||||
from mod.project.ssh.journalctlMod import JournalctlManage
|
||||
from mod.project.ssh.secureMod import SecureManage
|
||||
|
||||
|
||||
class main(JournalctlManage, SecureManage):
|
||||
|
||||
def __init__(self):
|
||||
super(main,self).__init__()
|
||||
|
||||
def get_ssh_list(self, get):
|
||||
"""
|
||||
@name 获取日志列表
|
||||
@param data:{"p":1,"limit":20,"search":"","select":"ALL"}
|
||||
@return list
|
||||
"""
|
||||
page = int(get.p) if hasattr(get, 'p') else 1
|
||||
limit = int(get.limit) if hasattr(get, 'limit') else 20
|
||||
query = get.get("search", "").strip().lower()
|
||||
history = get.get("historyType", "").strip().lower()
|
||||
|
||||
# 读取IP封禁规则
|
||||
ip_rules_file = "data/ssh_deny_ip_rules.json"
|
||||
try:
|
||||
ip_rules = json.loads(public.readFile(ip_rules_file))
|
||||
except Exception:
|
||||
ip_rules = []
|
||||
|
||||
login_type = self.login_all_flag
|
||||
get.select = get.get("select", "ALL")
|
||||
if get.select == "Failed":
|
||||
login_type = self.login_failed_flag
|
||||
elif get.select == "Accepted":
|
||||
login_type = self.login_access_flag
|
||||
|
||||
if history == "all":
|
||||
self.ssh_log_path += "*"
|
||||
total,login_list = self.get_secure_logs(login_type=login_type,pagesize=limit, page=page, query=query)
|
||||
|
||||
for log in login_list:
|
||||
if log["address"] in ip_rules:
|
||||
log["deny_status"] = 1
|
||||
data = self.return_area(login_list, 'address')
|
||||
return public.return_message(0, 0, {"data":data, "total":total})
|
||||
|
||||
def get_ssh_intrusion(self, get):
|
||||
"""
|
||||
@name 登陆详情统计 周期 昨天/今天 类型 成功/失败
|
||||
@return {"error": 0, "success": 0, "today_error": 0, "today_success": 0}
|
||||
"""
|
||||
stats = {
|
||||
'error': 0,
|
||||
'success': 0,
|
||||
'today_error': 0,
|
||||
'today_success': 0,
|
||||
'yesterday_error': 0,
|
||||
'yesterday_success': 0,
|
||||
'sevenday_error': 0,
|
||||
'sevenday_success': 0
|
||||
}
|
||||
try:
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 获取并更新日志数据
|
||||
today = datetime.now()
|
||||
yesterday = today - timedelta(days=1)
|
||||
|
||||
osv = public.get_os_version().lower()
|
||||
#个别系统使用标准时间格式
|
||||
date_v1 = ["debian", "opencloudos"]
|
||||
is_iso_date = any(d in osv for d in date_v1)
|
||||
if is_iso_date:
|
||||
# Debian/OpenCloudOS 日志为标准时间
|
||||
today_str = today.strftime("%Y-%m-%d")
|
||||
yesterday_str = yesterday.strftime("%Y-%m-%d")
|
||||
else:
|
||||
#centos ubuntu 等日志为月份日期
|
||||
today_str = today.strftime("%b %d").replace(" 0", " ")
|
||||
yesterday_str = yesterday.strftime("%b %d").replace(" 0", " ")
|
||||
|
||||
stats['today_error'] = self.get_secure_log_count(self.login_failed_flag, today_str)
|
||||
stats['today_success'] = self.get_secure_log_count(self.login_access_flag, today_str)
|
||||
stats['yesterday_success'] = self.get_secure_log_count(self.login_access_flag, yesterday_str)
|
||||
stats['yesterday_error'] = self.get_secure_log_count(self.login_failed_flag, yesterday_str)
|
||||
stats['sevenday_error'] = self.get_secure_log_count(self.login_failed_flag, "")
|
||||
stats['sevenday_success'] = self.get_secure_log_count(self.login_access_flag, "")
|
||||
|
||||
self.ssh_log_path += "*"
|
||||
stats['error'] = self.get_secure_log_count(self.login_failed_flag)
|
||||
stats['success'] = self.get_secure_log_count(self.login_access_flag)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
public.print_log(f"Failed to get SSH login information: {traceback.format_exc()}")
|
||||
return public.return_message(0, 0,stats)
|
||||
|
||||
def clean_ssh_list(self, get):
|
||||
"""
|
||||
@name 清空SSH登录记录 只保留最近一周的数据(从周日开始为一周)
|
||||
@return: {"status": True, "msg": "清空成功"}
|
||||
"""
|
||||
|
||||
public.ExecShell("rm -rf /var/log/secure-*;rm -rf /var/log/auth.log.*".format())
|
||||
|
||||
return public.return_message(0, 0, 'Clearance successful.')
|
||||
|
||||
def index_ssh_info(self, get):
|
||||
"""
|
||||
获取今天和昨天的SSH登录统计
|
||||
@return: list [今天登录次数, 昨天登录次数]
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
today_count = 0
|
||||
yesterday_count = 0
|
||||
|
||||
try:
|
||||
# 获取并更新日志数据
|
||||
today = datetime.now()
|
||||
yesterday = today - timedelta(days=1)
|
||||
|
||||
if "debian" in public.get_os_version().lower():
|
||||
today_str = today.strftime("%Y-%m-%d")
|
||||
yesterday_str = yesterday.strftime("%Y-%m-%d")
|
||||
else:
|
||||
today_str = today.strftime("%b %d").replace(" 0", " ")
|
||||
yesterday_str = yesterday.strftime("%b %d").replace(" 0", " ")
|
||||
|
||||
today_count = self.get_secure_log_count(self.login_all_flag, today_str)
|
||||
yesterday_count = self.get_secure_log_count(self.login_all_flag, yesterday_str)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
public.print_log(f"Failed to count SSH login information: {traceback.format_exc()}")
|
||||
|
||||
return [today_count, yesterday_count]
|
||||
|
||||
def add_cron_job(self,get):
|
||||
"""
|
||||
将 SSH爆破的脚本 添加到定时任务中
|
||||
"""
|
||||
cron_hour = get.get("cron_hour", 1)
|
||||
fail_count = get.get("fail_count", 10)
|
||||
ban_hour = get.get("ban_hour", 10)
|
||||
public.print_log(f"{cron_hour},{fail_count},{ban_hour}")
|
||||
cron_exist = public.M('crontab').where("name='aa-SSH Blast IP Blocking [Security - SSH Admin - Add to Login Logs]'", ()).get()
|
||||
if len(cron_exist) > 0:
|
||||
return public.return_message(-1, 0, 'Timed tasks already exist! Task details can be viewed in the panel scheduled tasks')
|
||||
|
||||
|
||||
from time import localtime
|
||||
run_minute = localtime().tm_min + 1
|
||||
if run_minute == 60: run_minute = 0
|
||||
|
||||
get.name = "aa-SSH Blast IP Blocking [Security - SSH Admin - Add to Login Logs]"
|
||||
get.type = "hour-n"
|
||||
get.hour = cron_hour
|
||||
get.minute = run_minute
|
||||
get.where1 = cron_hour
|
||||
get.where_hour = cron_hour
|
||||
get.week = "1"
|
||||
get.timeType = "sday"
|
||||
get.timeSet = "1"
|
||||
get.sType = "toShell"
|
||||
get.sBody = "{path}/pyenv/bin/python3 -u {path}/script/ssh_ban_login_failed.py {cron_hour} {fail_count} {ban_second}".format(
|
||||
path = public.get_panel_path(),
|
||||
cron_hour = cron_hour,
|
||||
fail_count = fail_count,
|
||||
ban_second = ban_hour * 3600
|
||||
)
|
||||
get.sName = ""
|
||||
get.backupTo = ""
|
||||
get.save = ""
|
||||
get.urladdress = ""
|
||||
get.save_local = "0"
|
||||
get.notice = "0"
|
||||
get.notice_channel = ""
|
||||
get.datab_name = ""
|
||||
get.tables_name = ""
|
||||
get.keyword = ""
|
||||
get.flock = "1"
|
||||
get.stop_site = "0"
|
||||
get.version = ""
|
||||
get.user = "root"
|
||||
from crontab import crontab
|
||||
|
||||
res = crontab().AddCrontab(get)
|
||||
if res["status"] == True:
|
||||
return public.return_message(0, 0,"Added successfully, the task will run at {} minutes per {} hour.".format(cron_hour,run_minute))
|
||||
public.set_module_logs('SSH', 'add_cron_job', 1)
|
||||
return res
|
||||
|
||||
def remove_cron_job(self,get):
|
||||
"""
|
||||
将 SSH爆破的脚本 在定时任务中移除
|
||||
"""
|
||||
cron_exist = public.M('crontab').where("name='aa-SSH Blast IP Blocking [Security - SSH Admin - Add to Login Logs]'", ()).get()
|
||||
if len(cron_exist) > 0:
|
||||
for crontask in cron_exist:
|
||||
get.id = crontask["id"]
|
||||
from crontab import crontab
|
||||
crontab().DelCrontab(get)
|
||||
return public.return_message(0 ,0, 'Timed tasks have been removed!')
|
||||
else:
|
||||
return public.return_message(-1, 0, 'Removal failed, timed task does not exist!')
|
||||
|
||||
def run_ban_login_failed_ip(self,get):
|
||||
hour = get.get("hour", 1)
|
||||
fail_count = get.get("fail_count", 10)
|
||||
ban_hour = get.get("ban_hour", 10)
|
||||
|
||||
exec_shell = "{path}/pyenv/bin/python3 -u {path}/script/ssh_ban_login_failed.py {hour} {fail_count} {ban_second}".format(
|
||||
path=public.get_panel_path(),
|
||||
hour=hour,
|
||||
fail_count=fail_count,
|
||||
ban_second=ban_hour * 3600
|
||||
)
|
||||
import panelTask
|
||||
task_obj = panelTask.bt_task()
|
||||
task_id = task_obj.create_task('SSH blocking and IP bursting programme', 0, exec_shell)
|
||||
public.set_module_logs('SSH', 'run_ban_login_failed_ip', 1)
|
||||
return {'status': True, 'msg': 'Task created.', 'task_id': task_id}
|
||||
57
mod/project/ssh/journalctlMod.py
Normal file
57
mod/project/ssh/journalctlMod.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
import public
|
||||
|
||||
from mod.project.ssh.base import SSHbase
|
||||
|
||||
|
||||
class JournalctlManage(SSHbase):
|
||||
def __init__(self):
|
||||
super(JournalctlManage, self).__init__()
|
||||
|
||||
def get_journalctl_logs(self, file_positions):
|
||||
'''
|
||||
获取 systemd journalctl 的 SSH 登录日志
|
||||
return 日志,游标位置
|
||||
'''
|
||||
new_logins = []
|
||||
current_positions = ""
|
||||
|
||||
command_list = [
|
||||
"journalctl -u ssh --no-pager --show-cursor --grep='Accepted|Failed password for|Accepted publickey'", # 全量获取
|
||||
"journalctl -u ssh --since '30 days ago' --no-pager --show-cursor --grep='Accepted|Failed password for|Accepted publickey'", # 30天
|
||||
"journalctl -u ssh --no-pager --show-cursor --grep='Accepted|Failed password for|Accepted publickey' --cursor='{}'".format(file_positions) # 从记录的游标开始读取
|
||||
]
|
||||
|
||||
if not file_positions:
|
||||
# 获取systemd日志所占用的空间
|
||||
res, err = public.ExecShell("journalctl --disk-usage")
|
||||
total_bytes = public.parse_journal_disk_usage(res)
|
||||
limit_bytes = 5 * 1024 * 1024 * 1024
|
||||
# 大于5G 取30天的数据量
|
||||
command = command_list[1] if total_bytes > limit_bytes else command_list[0]
|
||||
content = public.ExecShell(command)[0].strip()
|
||||
else:
|
||||
content = public.ExecShell(command_list[2])[0].strip()
|
||||
|
||||
lines = content.split('\n')
|
||||
if lines:
|
||||
# 处理去除多余游标字符
|
||||
current_positions = lines[-1].replace("-- cursor: ", "")
|
||||
|
||||
for line in lines[:-1]:
|
||||
if "No entries" in line:break
|
||||
|
||||
if any(keyword in line for keyword in ["Accepted password", "Failed password", "Accepted publickey"]):
|
||||
parts = line.split()
|
||||
year = datetime.now().year
|
||||
entry = self.parse_login_entry(parts, year)
|
||||
if entry:
|
||||
entry["log_file"] = "journalctl"
|
||||
new_logins.append(entry)
|
||||
return new_logins, current_positions
|
||||
103
mod/project/ssh/secureMod.py
Normal file
103
mod/project/ssh/secureMod.py
Normal file
@@ -0,0 +1,103 @@
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
import public
|
||||
|
||||
from mod.project.ssh.base import SSHbase
|
||||
|
||||
|
||||
class SecureManage(SSHbase):
|
||||
def __init__(self):
|
||||
super(SecureManage, self).__init__()
|
||||
self.login_access_flag = "Accepted"
|
||||
self.login_failed_flag = "Failed password"
|
||||
self.login_all_flag = "Failed password|Accepted"
|
||||
if os.path.exists("/var/log/auth.log"):
|
||||
self.ssh_log_path = "/var/log/auth.log"
|
||||
elif os.path.exists("/var/log/secure"):
|
||||
self.ssh_log_path = "/var/log/secure"
|
||||
else:
|
||||
self.ssh_log_path = "/var/log/message"
|
||||
|
||||
def execshell(self, commands):
|
||||
"""
|
||||
执行shell命令并返回结果。
|
||||
仅适用于 获取需要通过 标准输出和标准错误输出的命令。
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
commands,
|
||||
shell=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
executable="/bin/bash"
|
||||
)
|
||||
count = int(result.stdout.strip())
|
||||
datas = result.stderr.strip().split("\n")
|
||||
except Exception as e:
|
||||
count = 0
|
||||
datas = []
|
||||
return count, datas
|
||||
|
||||
def get_secure_logs(self,login_type,pagesize=10,page=1,query=''):
|
||||
"""
|
||||
读取SSH日志文件的内容。
|
||||
:param login_type: ssh登录类型 失败'Failed password' 成功'Accepted' 全部'Failed password|Accepted'
|
||||
:param pagesize: 每页显示的条数
|
||||
:param page: 当前页码
|
||||
:param query: 关键字搜索 ip or user or time
|
||||
:return: 日志内容的列表
|
||||
"""
|
||||
new_logins = []
|
||||
end = pagesize * page
|
||||
|
||||
danger_symbol = ['&', '&&', '||', '|', ';']
|
||||
for d in danger_symbol:
|
||||
if d in query:
|
||||
return new_logins
|
||||
|
||||
if query != '':
|
||||
query = "|grep -aE '{}'".format(query)
|
||||
commands = "ls -tr {file_path}|grep -v '\.gz$'|xargs cat|grep -aE '({login_type})'{query}| tee >(tail -n {end}|head -n {pagesize}|tac >&2)|wc -l".format(
|
||||
file_path=self.ssh_log_path,
|
||||
login_type=login_type,
|
||||
query=query,
|
||||
end=end,
|
||||
pagesize=pagesize)
|
||||
count,datas = self.execshell(commands)
|
||||
|
||||
year = datetime.now().year
|
||||
for line in datas:
|
||||
parts = line.split()
|
||||
if not parts:
|
||||
continue
|
||||
entry = self.parse_login_entry(parts, year)
|
||||
if entry:
|
||||
new_logins.append(entry)
|
||||
return count,new_logins
|
||||
|
||||
def get_secure_log_count(self,login_type,query=''):
|
||||
"""
|
||||
读取SSH日志文件的内容 统计登陆类型的条数。
|
||||
|
||||
:param login_type: ssh登录类型 失败'Failed password' 成功'Accepted' 全部'Failed password|Accepted'
|
||||
:param query: 关键字搜索 ip or user or time
|
||||
:return: 日志内容的列表
|
||||
"""
|
||||
|
||||
danger_symbol = ['&', '&&', '||', '|', ';']
|
||||
for d in danger_symbol:
|
||||
if d in query:
|
||||
return 0
|
||||
|
||||
if query != '':
|
||||
query = "|grep -a '{}'".format(query)
|
||||
commands = "ls -tr {file_path}|grep -v '\.gz$'|xargs cat|grep -aE '({login_type})'{query}|wc -l".format(file_path=self.ssh_log_path,login_type=login_type,query=query)
|
||||
result, err = public.ExecShell(commands)
|
||||
return int(result.strip())
|
||||
Reference in New Issue
Block a user