mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-19 22:25:45 +02:00
354 lines
14 KiB
Python
354 lines
14 KiB
Python
import json
|
|
import os.path
|
|
import time
|
|
import traceback
|
|
from typing import Optional, Tuple, Callable, Union, Dict, List
|
|
from mod.base.ssh_executor import SSHExecutor, CommandResult
|
|
from mod.project.node.dbutil import ServerNodeDB, Node
|
|
from mod.project.node.nodeutil.monitor_service import build_service_groups, normalize_service_targets, service_unknown_statuses
|
|
|
|
import public
|
|
|
|
def is_much_difference(a:float, b:float)->bool:
|
|
if a == 0 or b == 0:
|
|
return True
|
|
ratio = a / b
|
|
return ratio >= 10 or ratio <= 0.1
|
|
|
|
|
|
def _shell_single_quote(value: str) -> str:
|
|
return "'" + value.replace("'", "'\"'\"'") + "'"
|
|
|
|
|
|
class SSHApi:
|
|
is_local = False
|
|
_local_scripts_dir = os.path.join(os.path.dirname(__file__), "ssh_warp_scripts")
|
|
|
|
def __init__(self, host, port: int=22, username: str="root", password=None, pkey=None,
|
|
pkey_passwd=None, threading_mod=False, timeout=20):
|
|
self._real_ssh_conf = {
|
|
"host": host,
|
|
"username": username,
|
|
"port": port,
|
|
"password": password,
|
|
"key_file": "",
|
|
"passphrase": pkey_passwd,
|
|
"key_data": pkey,
|
|
"strict_host_key_checking": False,
|
|
"allow_agent": False,
|
|
"look_for_keys": False,
|
|
"threading_mod": threading_mod,
|
|
"timeout": timeout,
|
|
}
|
|
self._ssh_executor: Optional[SSHExecutor] = None
|
|
|
|
|
|
@classmethod
|
|
def new_by_id(cls, node_id: int, threading_mod=False) -> Optional["SSHApi"]:
|
|
data = ServerNodeDB().get_node_by_id(node_id)
|
|
if not data or not isinstance(data, dict):
|
|
return None
|
|
data["ssh_conf"] = json.loads(data["ssh_conf"])
|
|
if not data["ssh_conf"]:
|
|
return None
|
|
data["ssh_conf"]["threading_mod"] = threading_mod
|
|
return cls(**data["ssh_conf"])
|
|
|
|
def _get_ssh_executor(self) -> SSHExecutor:
|
|
if self._ssh_executor:
|
|
return self._ssh_executor
|
|
self._ssh_executor = SSHExecutor(**self._real_ssh_conf)
|
|
return self._ssh_executor
|
|
|
|
def get_net_work(self) -> Tuple[Optional[dict], str]:
|
|
data, err = self._run_script("system_info.sh")
|
|
if err:
|
|
return None, err
|
|
if not data.exit_code == 0:
|
|
return None, data.stderr
|
|
try:
|
|
data = json.loads(data.stdout)
|
|
if isinstance(data, dict) and "cpu" in data and "mem" in data:
|
|
return self._tans_net_work_form_data(data), ""
|
|
return None, "data in wrong format: %s" % str(data)
|
|
except Exception as e:
|
|
return None, str(e)
|
|
|
|
def get_service_status(self, services) -> Tuple[List[dict], str]:
|
|
service_list = [item for item in normalize_service_targets(services) if int(item.get("enabled", 1)) == 1]
|
|
if not service_list:
|
|
return [], ""
|
|
service_args = " ".join(_shell_single_quote(item["target_key"]) for item in service_list)
|
|
cmd = """
|
|
for svc in {service_args}; do
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl is-active --quiet "$svc"
|
|
rc=$?
|
|
state=$(systemctl is-active "$svc" 2>/dev/null || true)
|
|
else
|
|
service "$svc" status >/dev/null 2>&1
|
|
rc=$?
|
|
if [ "$rc" = "0" ]; then state="active"; else state="inactive"; fi
|
|
fi
|
|
if [ "$rc" = "0" ]; then running=1; else running=0; fi
|
|
printf '%s\\t%s\\t%s\\n' "$svc" "$running" "$state"
|
|
done
|
|
""".format(service_args=service_args)
|
|
executor = None
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
exit_code, stdout, stderr = executor.run(cmd)
|
|
if exit_code != 0 and not stdout:
|
|
return service_unknown_statuses(service_list, stderr or "Service status check failed"), stderr
|
|
target_map = {item["target_key"]: item for item in service_list}
|
|
now = int(time.time())
|
|
result = []
|
|
for line in str(stdout or "").splitlines():
|
|
parts = line.split("\t")
|
|
if len(parts) < 3:
|
|
continue
|
|
key = parts[0].strip()
|
|
item = target_map.get(key)
|
|
if not item:
|
|
continue
|
|
running = parts[1].strip() == "1"
|
|
result.append({
|
|
"name": key,
|
|
"target_key": key,
|
|
"target_name": item["target_name"],
|
|
"status": 1 if running else 0,
|
|
"running": running,
|
|
"state": parts[2].strip() or ("active" if running else "inactive"),
|
|
"error_msg": "",
|
|
"ts": now,
|
|
})
|
|
if not result:
|
|
return service_unknown_statuses(service_list, stderr or "Service status data is empty"), stderr
|
|
return result, ""
|
|
except RuntimeError:
|
|
return service_unknown_statuses(service_list, "SSH connection failed"), "SSH connection failed"
|
|
except Exception as e:
|
|
return service_unknown_statuses(service_list, str(e)), str(e)
|
|
finally:
|
|
if executor:
|
|
executor.close()
|
|
|
|
def get_service_options(self) -> Tuple[Dict[str, list], str]:
|
|
cmd = r"""
|
|
check_status() {
|
|
svc="$1"
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
systemctl is-active --quiet "$svc"
|
|
rc=$?
|
|
state=$(systemctl is-active "$svc" 2>/dev/null || true)
|
|
else
|
|
service "$svc" status >/dev/null 2>&1
|
|
rc=$?
|
|
if [ "$rc" = "0" ]; then state="active"; else state="inactive"; fi
|
|
fi
|
|
if [ "$rc" = "0" ]; then running=1; else running=0; fi
|
|
printf '%s\t%s' "$running" "$state"
|
|
}
|
|
emit_service() {
|
|
family="$1"; key="$2"; label="$3"; paths="$4"
|
|
exists=0
|
|
old_ifs="$IFS"; IFS=":"
|
|
for p in $paths; do
|
|
if [ -e "$p" ]; then exists=1; break; fi
|
|
done
|
|
IFS="$old_ifs"
|
|
if [ "$exists" = "1" ]; then
|
|
status=$(check_status "$key")
|
|
printf 'SERVICE\t%s\t%s\t%s\t\t\t%s\n' "$family" "$key" "$label" "$status"
|
|
fi
|
|
}
|
|
emit_service nginx nginx Nginx "/www/server/nginx/sbin/nginx:/www/server/nginx/nginx/sbin/nginx:/etc/init.d/nginx"
|
|
emit_service mysql mysqld MySQL "/www/server/mysql/bin/mysqld:/etc/init.d/mysqld"
|
|
emit_service apache httpd Apache "/www/server/apache/bin/httpd:/etc/init.d/httpd"
|
|
emit_service redis redis Redis "/www/server/redis/src/redis-server:/etc/init.d/redis"
|
|
emit_service pure-ftpd pure-ftpd Pure-Ftpd "/www/server/pure-ftpd/sbin/pure-ftpd:/etc/init.d/pure-ftpd"
|
|
emit_service memcached memcached Memcached "/usr/local/memcached/bin/memcached:/etc/init.d/memcached"
|
|
emit_service mongodb mongodb MongoDB "/www/server/mongodb/bin/mongod:/etc/init.d/mongodb"
|
|
emit_service pgsql pgsql PostgreSQL "/www/server/pgsql/bin/postgres:/etc/init.d/pgsql"
|
|
if [ -d /www/server/php ]; then
|
|
for dir in /www/server/php/[0-9][0-9]; do
|
|
[ -d "$dir" ] || continue
|
|
ver=$(basename "$dir")
|
|
if [ -x "$dir/sbin/php-fpm" ] || [ -x "$dir/bin/php" ] || [ -e "/etc/init.d/php-fpm-$ver" ]; then
|
|
status=$(check_status "php-fpm-$ver")
|
|
major=$(printf '%s' "$ver" | cut -c1)
|
|
minor=$(printf '%s' "$ver" | cut -c2)
|
|
printf 'SERVICE\tphp\tphp-fpm-%s\tPHP %s.%s\t%s\t%s.%s\t%s\n' "$ver" "$major" "$minor" "$ver" "$major" "$minor" "$status"
|
|
fi
|
|
done
|
|
fi
|
|
"""
|
|
executor = None
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
exit_code, stdout, stderr = executor.run(cmd)
|
|
if exit_code != 0 and not stdout:
|
|
return {"services": [], "groups": []}, stderr or "Service discovery failed"
|
|
services = []
|
|
for line in str(stdout or "").splitlines():
|
|
parts = line.split("\t")
|
|
if len(parts) < 8 or parts[0] != "SERVICE":
|
|
continue
|
|
running = parts[6].strip() == "1"
|
|
services.append({
|
|
"target_type": "service",
|
|
"service_family": parts[1].strip(),
|
|
"target_key": parts[2].strip(),
|
|
"target_name": parts[3].strip(),
|
|
"version": parts[4].strip(),
|
|
"version_name": parts[5].strip(),
|
|
"enabled": 0,
|
|
"installed": 1,
|
|
"is_versioned": 1 if parts[1].strip() == "php" else 0,
|
|
"status": 1 if running else 0,
|
|
"running": running,
|
|
"state": parts[7].strip() or ("active" if running else "inactive"),
|
|
"sort": 0,
|
|
})
|
|
return {"services": services, "groups": build_service_groups(services)}, ""
|
|
except RuntimeError:
|
|
return {"services": [], "groups": []}, "SSH connection failed"
|
|
except Exception as e:
|
|
return {"services": [], "groups": []}, str(e)
|
|
finally:
|
|
if executor:
|
|
executor.close()
|
|
|
|
@staticmethod
|
|
def _tans_net_work_form_data(data: dict):
|
|
data["mem"]["memAvailable"] = round(data["mem"]["memAvailable"] / 1024 / 1024, 2)
|
|
data["mem"]["memBuffers"] = round(data["mem"]["memBuffers"] / 1024 / 1024, 2)
|
|
data["mem"]["memCached"] = round(data["mem"]["memCached"] / 1024 / 1024, 2)
|
|
data["mem"]["memFree"] = round(data["mem"]["memFree"] / 1024 / 1024, 2)
|
|
data["mem"]["memRealUsed"] = round(data["mem"]["memRealUsed"] / 1024 / 1024, 2)
|
|
data["mem"]["memShared"] = round(data["mem"]["memShared"] / 1024 / 1024, 2)
|
|
data["mem"]["memTotal"] = round(data["mem"]["memTotal"] / 1024 / 1024, 2)
|
|
data["physical_memory"]= round(data["physical_memory"] / 1024 / 1024, 2)
|
|
if is_much_difference(data["mem"]["memTotal"], data["physical_memory"]):
|
|
if data["mem"]["memTotal"] >= 1024:
|
|
data["mem"]["memNewTotal"] = "%.2fGB" % (data["mem"]["memTotal"] / 1024)
|
|
else:
|
|
data["mem"]["memNewTotal"] = "%.2fMB" % data["mem"]["memTotal"]
|
|
else:
|
|
if data["physical_memory"] >= 1024:
|
|
data["mem"]["memNewTotal"] = "%.2fGB" % (data["physical_memory"] / 1024)
|
|
else:
|
|
data["mem"]["memNewTotal"] = "%.2fMB" % data["physical_memory"]
|
|
return data
|
|
|
|
def _run_script(self, script_name: str) -> Tuple[Optional[CommandResult], str]:
|
|
local_file = os.path.join(self._local_scripts_dir, script_name)
|
|
if not os.path.exists(local_file):
|
|
return None, "Script does not exist"
|
|
executor = None
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
result = executor.execute_local_script_collect(local_file)
|
|
return result, ""
|
|
except RuntimeError:
|
|
return None, "SSH connection failed"
|
|
except Exception as e:
|
|
return None, str(e)
|
|
finally:
|
|
if executor:
|
|
executor.close()
|
|
|
|
def target_file_exits(self, target_file: str) -> Tuple[bool, str]:
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
result, err = executor.path_exists(target_file)
|
|
return result, err
|
|
except RuntimeError:
|
|
print(traceback.format_exc(), flush=True)
|
|
return False, "SSH connection failed"
|
|
except Exception as e:
|
|
print(traceback.format_exc(), flush=True)
|
|
return False, str(e)
|
|
|
|
def create_dir(self, path: str) -> Tuple[bool, str]:
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
result, err = executor.create_dir(path)
|
|
return result, err
|
|
except RuntimeError:
|
|
print(traceback.format_exc())
|
|
return False, "SSH connection failed"
|
|
except Exception as e:
|
|
return False, str(e)
|
|
|
|
def upload_file(self, filename: str, target_path: str, mode: str = "cover",
|
|
call_log: Callable[[int, str], None] = None) -> str:
|
|
|
|
if not os.path.isfile(filename):
|
|
return "File: {} does not exist".format(filename)
|
|
|
|
target_file = os.path.join(target_path, os.path.basename(filename))
|
|
path_info = self.path_info(target_file)
|
|
if isinstance(path_info, str):
|
|
return path_info
|
|
|
|
if path_info['exists'] and mode == "ignore":
|
|
call_log(0, "File upload:{} -> {},The target file already exists, skip uploading".format(filename, target_file))
|
|
return ""
|
|
if path_info['exists'] and mode == "rename":
|
|
upload_name = "{}_{}".format(os.path.basename(filename), public.md5(filename))
|
|
call_log(0, "File upload:{} -> {},The target file already exists, it will be renamed to {}".format(filename, target_file, upload_name))
|
|
else:
|
|
upload_name = os.path.basename(filename)
|
|
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
def progress_callback(current_size: int, total_size: int):
|
|
if total_size == 0:
|
|
return
|
|
call_log(current_size * 100 // total_size, "" )
|
|
executor.upload(filename, os.path.join(target_path, upload_name), progress_callback=progress_callback)
|
|
except RuntimeError:
|
|
print(traceback.format_exc(), flush=True)
|
|
return "SSH connection failed"
|
|
except Exception as e:
|
|
print(traceback.format_exc(), flush=True)
|
|
return str(e)
|
|
return ""
|
|
|
|
def upload_dir_check(self, target_file: str) -> str:
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
path_info = executor.path_info(target_file)
|
|
if not path_info['exists']:
|
|
return ""
|
|
if path_info['is_dir']:
|
|
return "The name path is not a directory"
|
|
return ""
|
|
except RuntimeError:
|
|
print(traceback.format_exc(), flush=True)
|
|
return "SSH connection failed"
|
|
except Exception as e:
|
|
print(traceback.format_exc(), flush=True)
|
|
return str(e)
|
|
|
|
def path_info(self, path: str) -> Union[str, Dict]:
|
|
try:
|
|
executor = self._get_ssh_executor()
|
|
executor.open()
|
|
path_info = executor.path_info(path)
|
|
return path_info
|
|
except RuntimeError as e:
|
|
print(traceback.format_exc(), flush=True)
|
|
return "SSH connection failed: {}".format(str(e))
|
|
except Exception as e:
|
|
print(traceback.format_exc(), flush=True)
|
|
return "Failed to obtain path information:{}".format(str(e))
|