Initial YakPanel commit
This commit is contained in:
12
mod/project/python/__init__.py
Normal file
12
mod/project/python/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: yakpanel
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
# ------------------------------
|
||||
# pythonModel app init
|
||||
# ------------------------------
|
||||
184
mod/project/python/environmentMod.py
Normal file
184
mod/project/python/environmentMod.py
Normal file
@@ -0,0 +1,184 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: yakpanel
|
||||
# -------------------------------------------------------------------
|
||||
# ------------------------------
|
||||
# Python Env app
|
||||
# ------------------------------
|
||||
import os.path
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
if "/www/server/panel" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel")
|
||||
|
||||
from mod.project.python.pyenv_tool import (
|
||||
EnvironmentReporter,
|
||||
EnvironmentManager,
|
||||
_SYS_BIN_PATH,
|
||||
python_manager_path,
|
||||
pyenv_path,
|
||||
)
|
||||
from mod.base import json_response
|
||||
EnvironmentReporter().init_report()
|
||||
|
||||
class main:
|
||||
@staticmethod
|
||||
def create_python_env(get):
|
||||
try:
|
||||
venv_name = get.venv_name.strip()
|
||||
src_python_bin = get.python_bin.strip()
|
||||
if "ps" in get:
|
||||
ps = get.ps.strip()
|
||||
else:
|
||||
ps = ""
|
||||
except:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
|
||||
if not re.match(r'^[a-zA-Z0-9_.-]+$', venv_name):
|
||||
return json_response(False, "Virtual environment name contains invalid characters")
|
||||
|
||||
ws_send = None
|
||||
if "_ws" in get:
|
||||
ws_send = lambda x: get._ws.send(json.dumps({"callback": "create_python_env", "result": {"log": x}}))
|
||||
res = EnvironmentManager().create_python_env(venv_name, src_python_bin, ps, ws_send)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
try:
|
||||
import public
|
||||
public.set_module_logs('python_project_env', 'create_python_env', 1)
|
||||
except:
|
||||
pass
|
||||
return json_response(True, msg="Created successfully")
|
||||
|
||||
@staticmethod
|
||||
def list_environment(get):
|
||||
if get and get.get("sort_not_use/s") in ("1", "true"):
|
||||
sort_not_use = True
|
||||
else:
|
||||
sort_not_use = False
|
||||
|
||||
em = EnvironmentManager()
|
||||
all_project_map = em.all_python_project_map()
|
||||
env_map = {
|
||||
i.bin_path: i.to_dict(
|
||||
project_name=all_project_map.get(i.bin_path, []),
|
||||
can_remove=i.can_remove,
|
||||
can_create=i.can_create,
|
||||
can_set_default=i.can_set_default,
|
||||
path_name=i.path_name,
|
||||
from_panel=any((i.bin_path.startswith(x) for x in
|
||||
(python_manager_path(), pyenv_path()))) and i.env_type == "system"
|
||||
) for i in em.all_env
|
||||
}
|
||||
data = list(env_map.values())
|
||||
if sort_not_use:
|
||||
data.sort(key=lambda x: (
|
||||
len(x["project_name"]),
|
||||
("venv", "conda", "system").index(x["type"]),
|
||||
any(i == os.path.dirname(x["bin_path"]) for i in _SYS_BIN_PATH)
|
||||
))
|
||||
else:
|
||||
data.sort(key=lambda x: (
|
||||
0 - len(x["project_name"]),
|
||||
("venv", "conda", "system").index(x["type"]),
|
||||
any(i == os.path.dirname(x["bin_path"]) for i in _SYS_BIN_PATH)
|
||||
))
|
||||
for i in data[::-1]:
|
||||
i["name"] = i["venv_name"] or i["path_name"] or i["version"]
|
||||
if i["type"] == "venv":
|
||||
i["system_data"] = env_map[i["system_path"]]
|
||||
i["can_remove"] = False if i["project_name"] else i["can_remove"]
|
||||
now_env = em.get_default_python_env()
|
||||
if now_env:
|
||||
now_env = env_map[now_env.bin_path]
|
||||
now_env["can_set_default"] = False
|
||||
|
||||
return json_response(True, data={"env_list": data, "now_env": now_env})
|
||||
|
||||
@staticmethod
|
||||
def add_environment(get):
|
||||
try:
|
||||
add_type = get.add_type.strip()
|
||||
path = get.path.strip()
|
||||
except:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
res = EnvironmentManager().add_python_env(add_type, path)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
try:
|
||||
import public
|
||||
public.set_module_logs('python_project_env', 'add_environment', 1)
|
||||
except:
|
||||
pass
|
||||
return json_response(True, msg="Added successfully")
|
||||
|
||||
@staticmethod
|
||||
def remove_environment(get):
|
||||
try:
|
||||
path_data = get.path_data.strip()
|
||||
except:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
if not path_data:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
em = EnvironmentManager()
|
||||
if "," in path_data:
|
||||
path_list = path_data.split(",")
|
||||
res = em.multi_remove_env(*path_list)
|
||||
status = all(x.get("status") for x in res)
|
||||
if not status:
|
||||
err_msg = "\n".join([x.get("msg", "") for x in res if not x.get("status")])
|
||||
return json_response(False, err_msg)
|
||||
return json_response(status, data=res)
|
||||
else:
|
||||
res = em.multi_remove_env(path_data)
|
||||
for r in res:
|
||||
if r.get("status"):
|
||||
return json_response(True, msg="Removed successfully")
|
||||
return json_response(False, r.get("msg", "Failed to remove"))
|
||||
return json_response(True, msg="Removed successfully")
|
||||
|
||||
@staticmethod
|
||||
def set_environment_default(get):
|
||||
try:
|
||||
path = get.path.strip()
|
||||
except:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
if not path or path == "close":
|
||||
path = ""
|
||||
em = EnvironmentManager()
|
||||
res = em.set_python2env(path)
|
||||
if not path:
|
||||
if not res:
|
||||
return json_response(True, msg="Disabled successfully")
|
||||
else:
|
||||
return json_response(False, res)
|
||||
else:
|
||||
if not res:
|
||||
return json_response(True, msg="Set successfully")
|
||||
else:
|
||||
return json_response(False, res)
|
||||
|
||||
@staticmethod
|
||||
def set_environment_ps(get):
|
||||
try:
|
||||
path = get.path.strip()
|
||||
ps = get.ps.strip()
|
||||
except:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
if not path or not ps:
|
||||
return json_response(False, 'Invalid parameters')
|
||||
em = EnvironmentManager()
|
||||
res = em.set_python_env_ps(path, ps)
|
||||
if not res:
|
||||
return json_response(True, msg="Set successfully")
|
||||
else:
|
||||
return json_response(False, res)
|
||||
1438
mod/project/python/pyenv_tool.py
Normal file
1438
mod/project/python/pyenv_tool.py
Normal file
File diff suppressed because it is too large
Load Diff
788
mod/project/python/serviceMod.py
Normal file
788
mod/project/python/serviceMod.py
Normal file
@@ -0,0 +1,788 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: yakpanel
|
||||
# -------------------------------------------------------------------
|
||||
# ------------------------------
|
||||
# py service model app
|
||||
# ------------------------------
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import psutil
|
||||
from uuid import uuid4
|
||||
from typing import Optional, Union, List, Dict, Tuple, Any, Set
|
||||
|
||||
SERVICE_PATH = "/www/server/python_project/service"
|
||||
if not os.path.isdir(SERVICE_PATH):
|
||||
try:
|
||||
os.makedirs(SERVICE_PATH, 0o755)
|
||||
except:
|
||||
pass
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
if "/www/server/panel" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel")
|
||||
|
||||
import public
|
||||
from mod.base import json_response
|
||||
from mod.project.python.pyenv_tool import EnvironmentManager
|
||||
from public.exceptions import HintException
|
||||
|
||||
|
||||
class Environment(object):
|
||||
def __init__(self, project_name: str, python_path: str, python_bin: str, project_path: str, user: str,
|
||||
env_list: List[Dict[str, str]], env_file: str):
|
||||
self.python_path = python_path
|
||||
self.project_path = project_path
|
||||
self.env_list = env_list
|
||||
self.env_file = env_file
|
||||
self.project_name = project_name
|
||||
self.user = user
|
||||
self._env_cache: Optional[str] = None
|
||||
self.pyenv = EnvironmentManager().get_env_py_path(python_bin)
|
||||
|
||||
@classmethod
|
||||
def form_project_conf(cls, project_config: dict) -> Union["Environment", str]:
|
||||
if not isinstance(project_config, dict):
|
||||
return 'Invalid project configuration file format'
|
||||
|
||||
python_path: str = project_config.get("vpath")
|
||||
python_bin: str = project_config.get("python_bin", project_config.get("vpath"))
|
||||
project_path: str = project_config.get("path")
|
||||
env_list = project_config.get("env_list", [])
|
||||
env_file = project_config.get("env_file", "")
|
||||
project_name = project_config.get("pjname")
|
||||
user = project_config.get("user", "root")
|
||||
if not python_path or not project_path or not project_name:
|
||||
return 'Invalid project configuration file format'
|
||||
|
||||
if not os.path.isdir(python_path) or not os.path.isdir(project_path):
|
||||
return 'The project directory or virtual environment directory specified in the config does not exist'
|
||||
|
||||
python_path = python_path.rstrip("/")
|
||||
project_path = project_path.rstrip("/")
|
||||
if not python_path.endswith("/bin"):
|
||||
python_path = python_path + "/bin"
|
||||
if not os.path.isdir(python_path):
|
||||
return 'The virtual environment directory specified in the config does not exist'
|
||||
|
||||
return cls(project_name, python_path, python_bin, project_path, user, env_list, env_file)
|
||||
|
||||
# 组合环境变量,用于启动服务
|
||||
def shell_env(self) -> str:
|
||||
if self._env_cache is not None:
|
||||
return self._env_cache
|
||||
|
||||
# cd 到指定路径, 加载环境变量, 加载环境变量文件, 设置Python环境到首位
|
||||
res_env_list = ["cd {}".format(self.project_path)]
|
||||
if isinstance(self.env_list, list):
|
||||
for i in self.env_list:
|
||||
if not isinstance(i, dict):
|
||||
continue
|
||||
if 'k' in i and 'v' in i:
|
||||
res_env_list.append("export {}={}".format(i['k'], i['v']))
|
||||
|
||||
if self.env_file and os.path.isfile(self.env_file):
|
||||
res_env_list.append("source {}".format(self.env_file))
|
||||
|
||||
res_env_list.append(self.pyenv.activate_shell())
|
||||
|
||||
self._env_cache = "\n".join(res_env_list)
|
||||
|
||||
return self._env_cache
|
||||
|
||||
|
||||
class PythonService(object):
|
||||
def __init__(self, sid: str, name: str, command: str, level: Optional[int], log_type: Optional[str]):
|
||||
self.sid = sid
|
||||
self.name = name
|
||||
self.command = command
|
||||
self.level = level
|
||||
self.log_type = log_type
|
||||
self.env: Optional[Environment] = None
|
||||
|
||||
def set_env(self, env: Environment):
|
||||
self.env = env
|
||||
|
||||
def write_pid(self, pid: int):
|
||||
if not self.env:
|
||||
raise RuntimeError('Env not set')
|
||||
pid_file = os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.env.project_name, self.name))
|
||||
if not os.path.isdir(os.path.dirname(pid_file)):
|
||||
os.makedirs(os.path.dirname(pid_file), 0o755)
|
||||
public.writeFile(pid_file, str(pid))
|
||||
|
||||
def read_pid(self) -> Optional[int]:
|
||||
if not self.env:
|
||||
raise RuntimeError('Env not set')
|
||||
pid_file = os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.env.project_name, self.name))
|
||||
if not os.path.isfile(pid_file):
|
||||
return None
|
||||
|
||||
res = None
|
||||
try:
|
||||
res = int(public.readFile(pid_file))
|
||||
except:
|
||||
pass
|
||||
if isinstance(res, int) and res > 0:
|
||||
return res
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def from_config(cls, config: dict,
|
||||
env: Optional[Environment]) -> Union['PythonService', "MainPythonService", 'CeleryService', str]:
|
||||
sid = config.get('sid', None)
|
||||
if sid == 'main':
|
||||
return MainPythonService(env)
|
||||
|
||||
name: str = config.get('name', "")
|
||||
command: str = config.get('command', "")
|
||||
if not sid or not name or not command:
|
||||
return 'Missing required parameters'
|
||||
|
||||
if not isinstance(command, str) or not isinstance(name, str) or not isinstance(sid, str):
|
||||
return 'Invalid parameter type'
|
||||
|
||||
level = config.get('level', 11)
|
||||
log_type = config.get('log_type', "append")
|
||||
|
||||
if command.split()[0].endswith("celery"):
|
||||
res = CeleryService(sid, name, command, level, log_type)
|
||||
else:
|
||||
res = cls(sid, name, command, level, log_type)
|
||||
|
||||
if env:
|
||||
res.set_env(env)
|
||||
return res
|
||||
|
||||
# 执行启动服务并返回PID信息或错误信息
|
||||
def start(self) -> Optional[int]:
|
||||
if not self.env:
|
||||
raise RuntimeError('Env not set')
|
||||
log_file = os.path.join(SERVICE_PATH, '{}/{}.log'.format(self.env.project_name, self.name))
|
||||
pid_file = os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.env.project_name, self.name))
|
||||
if not os.path.exists(os.path.dirname(pid_file)):
|
||||
os.makedirs(os.path.dirname(pid_file), 0o755)
|
||||
if os.path.exists(pid_file):
|
||||
os.remove(pid_file)
|
||||
prep_sh = self.env.shell_env()
|
||||
prep_sh += "\nexport BT_PYTHON_SERVICE_SID={}".format(self.sid)
|
||||
if not os.path.isfile(log_file):
|
||||
public.writeFile(log_file, '')
|
||||
public.set_own(log_file, self.env.user)
|
||||
public.set_mode(log_file, "755")
|
||||
if self.log_type == "append":
|
||||
prep_sh += "\nnohup {} &>> {} &".format(self.command, log_file)
|
||||
else:
|
||||
prep_sh += "\nnohup {} &> {} &".format(self.command, log_file)
|
||||
|
||||
public.ExecShell(prep_sh, user=self.env.user)
|
||||
time.sleep(0.5)
|
||||
return self.get_service_pid()
|
||||
|
||||
def get_service_pid(self, only_service: bool = False) -> Optional[int]:
|
||||
pid = self.read_pid()
|
||||
if pid and psutil.pid_exists(pid):
|
||||
return pid
|
||||
|
||||
if not pid:
|
||||
pid = self.get_pid_by_env_key()
|
||||
if not pid and not only_service:
|
||||
pid = self.get_pid_by_command()
|
||||
|
||||
if pid:
|
||||
self.write_pid(pid)
|
||||
return pid
|
||||
return None
|
||||
|
||||
def get_pid_by_env_key(self) -> Optional[int]:
|
||||
env_key = "BT_PYTHON_SERVICE_SID={}".format(self.sid)
|
||||
target = []
|
||||
for p in psutil.pids():
|
||||
try:
|
||||
data: str = public.readFile("/proc/{}/environ".format(p))
|
||||
if data.rfind(env_key) != -1:
|
||||
target.append(p)
|
||||
except:
|
||||
continue
|
||||
|
||||
for i in target:
|
||||
try:
|
||||
p = psutil.Process(i)
|
||||
if p.ppid() not in target:
|
||||
return i
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def get_pid_by_command(self) -> Optional[int]:
|
||||
cmd_list = self.split_command()
|
||||
target = []
|
||||
for p in psutil.process_iter(["cmdline", "pid", "exe"]):
|
||||
try:
|
||||
real_cmd = p.cmdline()
|
||||
if cmd_list == real_cmd:
|
||||
target.append(p)
|
||||
if real_cmd[2:] == cmd_list[1:] and real_cmd[0].startswith(self.env.python_path):
|
||||
target.append(p)
|
||||
except:
|
||||
continue
|
||||
|
||||
for p in target:
|
||||
try:
|
||||
if p.ppid() not in target:
|
||||
return p.pid
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
def split_command(self) -> List[str]:
|
||||
res = []
|
||||
tmp = ""
|
||||
in_quot = False
|
||||
for i in self.command:
|
||||
if i in (' ', '\t', '\r'):
|
||||
if tmp and not in_quot:
|
||||
res.append(tmp)
|
||||
tmp = ""
|
||||
if in_quot:
|
||||
tmp += ' '
|
||||
|
||||
elif i in ("'", '"'):
|
||||
if in_quot:
|
||||
in_quot = False
|
||||
else:
|
||||
in_quot = True
|
||||
else:
|
||||
tmp += i
|
||||
|
||||
if tmp:
|
||||
res.append(tmp)
|
||||
|
||||
return res
|
||||
|
||||
def stop(self) -> None:
|
||||
pid = self.get_service_pid()
|
||||
if not pid:
|
||||
return
|
||||
try:
|
||||
p = psutil.Process(pid)
|
||||
p.kill()
|
||||
except:
|
||||
pass
|
||||
|
||||
def get_log(self) -> str:
|
||||
if not self.env:
|
||||
raise RuntimeError('env not set')
|
||||
log_file = os.path.join(SERVICE_PATH, '{}/{}.log'.format(self.env.project_name, self.name))
|
||||
if not os.path.isfile(log_file):
|
||||
return 'No logs available'
|
||||
data = public.GetNumLines(log_file, 1000)
|
||||
if not data:
|
||||
return 'No logs available'
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _get_ports_by_pid(pid: int) -> List[int]:
|
||||
try:
|
||||
res = set()
|
||||
for con in psutil.Process(pid).connections(): # NOQA
|
||||
if con.status == 'LISTEN':
|
||||
res.add(con.laddr.port)
|
||||
return list(res)
|
||||
except:
|
||||
return []
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
if not self.env:
|
||||
raise RuntimeError('env not set')
|
||||
pid = self.get_service_pid()
|
||||
if isinstance(pid, int) and psutil.pid_exists(pid):
|
||||
ports = self._get_ports_by_pid(pid)
|
||||
return {
|
||||
'pid': pid,
|
||||
'ports': ports
|
||||
}
|
||||
return {"pid": None, "ports": []}
|
||||
|
||||
|
||||
class MainPythonService(PythonService):
|
||||
from projectModelV2.pythonModel import main as py_project_main
|
||||
_py_main_class = py_project_main
|
||||
|
||||
def __init__(self, env: Environment):
|
||||
super().__init__('main', 'main', 'main', 10, 'append')
|
||||
self.set_env(env)
|
||||
|
||||
@property
|
||||
def py_main(self):
|
||||
return self._py_main_class()
|
||||
|
||||
def start(self) -> Optional[int]:
|
||||
if not self.env:
|
||||
raise RuntimeError('Env not set')
|
||||
self.py_main.only_start_main_project(self.env.project_name)
|
||||
return self.get_service_pid()
|
||||
|
||||
def get_service_pid(self, only_service: bool = False) -> Optional[int]:
|
||||
if not self.env:
|
||||
raise RuntimeError('env not set')
|
||||
pids: List[int] = self.py_main.get_project_run_state(self.env.project_name)
|
||||
if not pids:
|
||||
return None
|
||||
pids.sort()
|
||||
return pids[0]
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self.env:
|
||||
raise RuntimeError('env not set')
|
||||
self.py_main.only_stop_main_project(self.env.project_name)
|
||||
|
||||
def get_log(self) -> str:
|
||||
if not self.env:
|
||||
raise RuntimeError('env not set')
|
||||
get_obj = public.dict_obj()
|
||||
get_obj.name = self.env.project_name
|
||||
res = self.py_main.GetProjectLog(get_obj)
|
||||
data = None
|
||||
if res.get("status"):
|
||||
data = res.get("message", {}).get("data", "")
|
||||
|
||||
if not data:
|
||||
return 'no log found'
|
||||
return data
|
||||
|
||||
def get_info(self):
|
||||
res = super().get_info()
|
||||
res['name'] = "Main project service"
|
||||
return res
|
||||
|
||||
|
||||
class CeleryService(PythonService):
|
||||
def get_celery_env(self) -> Tuple[str, str]:
|
||||
celery = "{}/celery".format(self.env.python_path)
|
||||
if not os.path.isfile(celery):
|
||||
return '', ''
|
||||
celery_data = public.readFile(celery)
|
||||
if not isinstance(celery_data, str):
|
||||
return '', ''
|
||||
celery_python = celery_data.split("\n", 1)[0]
|
||||
if celery_python.startswith("#!"):
|
||||
celery_python = celery_python[2:].strip()
|
||||
return celery_python, celery
|
||||
|
||||
def get_pid_by_command(self) -> Optional[int]:
|
||||
celery_env = self.get_celery_env()
|
||||
if not celery_env[0] or not celery_env[1]:
|
||||
return super().get_pid_by_command()
|
||||
target = []
|
||||
cmd_list = list(celery_env) + self.split_command()[1:]
|
||||
for p in psutil.process_iter(["cmdline", "pid"]):
|
||||
try:
|
||||
if cmd_list == p.cmdline():
|
||||
target.append(p)
|
||||
except:
|
||||
continue
|
||||
|
||||
for p in target:
|
||||
try:
|
||||
if p.ppid() not in target:
|
||||
return p.pid
|
||||
except:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
# 协同服务管理类, 包括主服务和其他服务
|
||||
class ServiceManager:
|
||||
MAIN_SERVICE_CONF = {
|
||||
"sid": "main",
|
||||
"name": "main",
|
||||
"command": "main",
|
||||
"level": 10,
|
||||
"log_type": "append",
|
||||
}
|
||||
|
||||
def __init__(self, project_name: str, project_config: dict):
|
||||
self.project_name = project_name
|
||||
self.project_config = project_config
|
||||
self._other_services: Optional[List[Dict]] = None
|
||||
self._env: Optional[Environment] = None
|
||||
|
||||
@classmethod
|
||||
def new_mgr(cls, project_name: str) -> Union["ServiceManager", str]:
|
||||
data = public.M("sites").where(
|
||||
'project_type=? AND name=? ', ('Python', project_name)
|
||||
).field('id,project_config').find()
|
||||
if not data:
|
||||
raise HintException("Project [{}] Not Found!".format(project_name))
|
||||
try:
|
||||
project_config = json.loads(data['project_config'])
|
||||
except json.JSONDecodeError:
|
||||
raise HintException("Project [{}] db's Project Config Error!".format(project_name))
|
||||
return cls(project_name, project_config)
|
||||
|
||||
@property
|
||||
def service_list(self) -> List[Dict]:
|
||||
res = [self.MAIN_SERVICE_CONF]
|
||||
res.extend(self.other_services)
|
||||
res.sort(key=lambda x: x['level'])
|
||||
return res
|
||||
|
||||
@property
|
||||
def other_services(self) -> List[Dict]:
|
||||
if self._other_services is None:
|
||||
services = []
|
||||
for service in self.project_config.get('services', []):
|
||||
if service.get('sid') == 'main':
|
||||
continue
|
||||
services.append(service)
|
||||
self._other_services = services
|
||||
return self._other_services
|
||||
|
||||
@staticmethod
|
||||
def new_id() -> str:
|
||||
return uuid4().hex[::3]
|
||||
|
||||
def save_service_conf(self) -> Optional[str]:
|
||||
data = public.M("sites").where(
|
||||
'project_type=? AND name=? ', ('Python', self.project_name)
|
||||
).field('id,project_config').find()
|
||||
if not data:
|
||||
return "Website information not found"
|
||||
data['project_config'] = json.loads(data['project_config'])
|
||||
data['project_config']['services'] = self.other_services
|
||||
public.M("sites").where('id=?', (data['id'],)).update({'project_config': json.dumps(data['project_config'])})
|
||||
return None
|
||||
|
||||
def add_service(self, service_conf: dict) -> Optional[str]:
|
||||
try:
|
||||
conf = {
|
||||
"name": service_conf.get("name", "").strip(),
|
||||
"command": service_conf.get("command", "").strip(),
|
||||
"level": int(service_conf.get("level", 11)),
|
||||
"log_type": service_conf.get("log_type", "append"),
|
||||
}
|
||||
except:
|
||||
return "Parameter error"
|
||||
|
||||
if re.search(r"[\s$^`]+", conf['name']):
|
||||
return "Service name cannot contain spaces or special characters"
|
||||
|
||||
for i in self.other_services:
|
||||
if i['name'] == conf['name']:
|
||||
return "Service name must be unique"
|
||||
if i['command'] == conf['command']:
|
||||
return "This start command already exists; service name: {}".format(i['name'])
|
||||
|
||||
if not (conf['name'] and conf['command']):
|
||||
return "Service name and start command cannot be empty"
|
||||
|
||||
conf["sid"] = self.new_id()
|
||||
|
||||
self.other_services.append(conf)
|
||||
self.save_service_conf()
|
||||
return None
|
||||
|
||||
def modify_service(self, sid: str, service_conf: dict) -> Optional[str]:
|
||||
target_data = None
|
||||
for i in self.other_services:
|
||||
if i["sid"] == sid:
|
||||
target_data = i
|
||||
break
|
||||
if target_data is None:
|
||||
return "Service not found"
|
||||
|
||||
name = target_data["name"]
|
||||
if "name" in service_conf and service_conf["name"] != target_data["name"]:
|
||||
name = service_conf["name"].strip()
|
||||
if re.search(r"[\s$^`]+", name):
|
||||
return "Service name cannot contain spaces or special characters"
|
||||
command = target_data["command"]
|
||||
if "command" in service_conf and service_conf["command"] != target_data["command"]:
|
||||
command = service_conf["command"].strip()
|
||||
|
||||
for i in self.other_services:
|
||||
if i["sid"] == sid:
|
||||
continue
|
||||
if i["name"] == name:
|
||||
return "Service name must be unique"
|
||||
if i["command"] == command:
|
||||
return "This start command already exists; service name: {}".format(i["name"])
|
||||
|
||||
if name != target_data["name"]:
|
||||
log_file = os.path.join(SERVICE_PATH, '{}/{}.log'.format(self.project_name, target_data["name"]))
|
||||
pid_file = os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.project_name, target_data["name"]))
|
||||
if os.path.exists(log_file):
|
||||
os.rename(log_file, os.path.join(SERVICE_PATH, '{}/{}.log'.format(self.project_name, name)))
|
||||
if os.path.exists(pid_file):
|
||||
os.rename(pid_file, os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.project_name, name)))
|
||||
|
||||
target_data["name"] = name
|
||||
|
||||
target_data["command"] = command
|
||||
target_data["level"] = int(service_conf.get("level", int(target_data.get("level", 11))))
|
||||
target_data["log_type"] = service_conf.get("log_type", target_data["log_type"])
|
||||
self.save_service_conf()
|
||||
return None
|
||||
|
||||
def remove_service(self, sid: str) -> Optional[str]:
|
||||
del_idx = None
|
||||
for idx, i in enumerate(self.other_services):
|
||||
if i["sid"] == sid:
|
||||
del_idx = idx
|
||||
break
|
||||
|
||||
if del_idx is None:
|
||||
return "Service not found"
|
||||
del_conf = self.other_services.pop(del_idx)
|
||||
self.save_service_conf()
|
||||
log_file = os.path.join(SERVICE_PATH, '{}/{}.log'.format(self.project_name, del_conf["name"]))
|
||||
pid_file = os.path.join(SERVICE_PATH, '{}/{}.pid'.format(self.project_name, del_conf["name"]))
|
||||
if os.path.exists(log_file):
|
||||
os.remove(log_file)
|
||||
if os.path.exists(pid_file):
|
||||
os.remove(pid_file)
|
||||
return None
|
||||
|
||||
def _get_service_conf_by_sid(self, sid: str) -> Optional[Dict]:
|
||||
for i in self.service_list:
|
||||
if i["sid"] == sid:
|
||||
return i
|
||||
return None
|
||||
|
||||
def _build_service_by_conf(self, conf: dict) -> Union[PythonService, str]:
|
||||
if not self._env:
|
||||
self._env = Environment.form_project_conf(self.project_config)
|
||||
if isinstance(self._env, str):
|
||||
return self._env
|
||||
return PythonService.from_config(conf, env=self._env)
|
||||
|
||||
def handle_service(self, sid: str, action: str = "start") -> Optional[str]:
|
||||
conf = self._get_service_conf_by_sid(sid)
|
||||
if conf is None:
|
||||
return "Service not found"
|
||||
|
||||
service = self._build_service_by_conf(conf)
|
||||
if isinstance(service, str):
|
||||
return service
|
||||
pid = service.get_service_pid()
|
||||
if not pid:
|
||||
pid = -1
|
||||
if action == "start":
|
||||
if not psutil.pid_exists(pid):
|
||||
service.start()
|
||||
elif action == "stop":
|
||||
service.stop()
|
||||
elif action == "restart":
|
||||
if psutil.pid_exists(pid):
|
||||
service.stop()
|
||||
for i in range(50):
|
||||
if not psutil.pid_exists(pid):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
service.stop()
|
||||
time.sleep(1)
|
||||
|
||||
service.start()
|
||||
else:
|
||||
return "Unknown action"
|
||||
|
||||
return None
|
||||
|
||||
def get_service_log(self, sid: str) -> Tuple[bool, str]:
|
||||
conf = self._get_service_conf_by_sid(sid)
|
||||
if conf is None:
|
||||
return False, "Service Not Found"
|
||||
service = self._build_service_by_conf(conf)
|
||||
if isinstance(service, str):
|
||||
return False, service
|
||||
return True, service.get_log()
|
||||
|
||||
def get_services_info(self) -> List[dict]:
|
||||
res = []
|
||||
for i in self.service_list:
|
||||
service = self._build_service_by_conf(i)
|
||||
if isinstance(service, str):
|
||||
i["error"] = service
|
||||
res.append(i)
|
||||
else:
|
||||
i.update(service.get_info())
|
||||
res.append(i)
|
||||
|
||||
return res
|
||||
|
||||
def start_project(self):
|
||||
services = [
|
||||
self._build_service_by_conf(i) for i in self.service_list
|
||||
]
|
||||
for i in services:
|
||||
try:
|
||||
if isinstance(i, str):
|
||||
continue
|
||||
pid = i.get_service_pid()
|
||||
if isinstance(pid, int) and pid > 0 and psutil.pid_exists(pid):
|
||||
continue
|
||||
i.start()
|
||||
time.sleep(0.5)
|
||||
except Exception:
|
||||
import traceback
|
||||
public.print_log("start project service error: {}".format(traceback.format_exc()))
|
||||
continue
|
||||
return True
|
||||
|
||||
def stop_project(self):
|
||||
services = [self._build_service_by_conf(i) for i in self.service_list]
|
||||
for i in services[::-1]:
|
||||
if isinstance(i, str):
|
||||
continue
|
||||
i.stop()
|
||||
return "Stop command executed"
|
||||
|
||||
def other_service_pids(self) -> Set[int]:
|
||||
res_pid = []
|
||||
for i in self.other_services:
|
||||
service = self._build_service_by_conf(i)
|
||||
if isinstance(service, str):
|
||||
continue
|
||||
pid = service.get_service_pid()
|
||||
if isinstance(pid, int) and pid > 0 and psutil.pid_exists(pid):
|
||||
res_pid.append(pid)
|
||||
|
||||
sub_pid = []
|
||||
|
||||
def get_sub_pid(pro: psutil.Process) -> List[int]:
|
||||
tmp_res = []
|
||||
if pro.status() != psutil.STATUS_ZOMBIE and pro.children():
|
||||
for sub_pro in pro.children():
|
||||
tmp_res.append(sub_pro.pid)
|
||||
tmp_res.extend(get_sub_pid(sub_pro))
|
||||
return tmp_res
|
||||
|
||||
for i in res_pid:
|
||||
try:
|
||||
p = psutil.Process(i)
|
||||
sub_pid.extend(get_sub_pid(p))
|
||||
except:
|
||||
pass
|
||||
|
||||
return set(res_pid + sub_pid)
|
||||
|
||||
|
||||
# 协同服务api
|
||||
class main:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_services_info(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
return json_response(True, data=s_mgr.get_services_info())
|
||||
|
||||
@staticmethod
|
||||
def add_service(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
service_conf = get.service_conf
|
||||
if isinstance(service_conf, str):
|
||||
service_conf = json.loads(service_conf)
|
||||
if not isinstance(service_conf, dict):
|
||||
return json_response(False, 'Invalid service configuration parameters')
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
res = s_mgr.add_service(service_conf)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
public.set_module_logs('python_project', 'add_service', 1)
|
||||
return json_response(True, msg="Added successfully")
|
||||
|
||||
@staticmethod
|
||||
def modify_service(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
sid = get.sid.strip()
|
||||
service_conf = get.service_conf
|
||||
if isinstance(service_conf, str):
|
||||
service_conf = json.loads(service_conf)
|
||||
if not isinstance(service_conf, dict):
|
||||
return json_response(False, 'Invalid service configuration parameters')
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
|
||||
res = s_mgr.modify_service(sid, service_conf)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
return json_response(True, msg="Updated successfully")
|
||||
|
||||
@staticmethod
|
||||
def remove_service(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
sid = get.sid.strip()
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
res = s_mgr.remove_service(sid)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
|
||||
return json_response(True, msg="Deleted successfully")
|
||||
|
||||
@staticmethod
|
||||
def handle_service(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
sid = get.sid.strip()
|
||||
action = get.option.strip()
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
res = s_mgr.handle_service(sid, action)
|
||||
if isinstance(res, str):
|
||||
return json_response(False, res)
|
||||
|
||||
return json_response(True, msg="Operation successful")
|
||||
|
||||
@staticmethod
|
||||
def get_service_log(get):
|
||||
try:
|
||||
project_name = get.project_name.strip()
|
||||
sid = get.sid.strip()
|
||||
except:
|
||||
return json_response(False, 'Parameter error')
|
||||
s_mgr = ServiceManager.new_mgr(project_name)
|
||||
if isinstance(s_mgr, str):
|
||||
return json_response(False, s_mgr)
|
||||
res, log = s_mgr.get_service_log(sid)
|
||||
if not res:
|
||||
return json_response(False, log)
|
||||
return json_response(True, data=log)
|
||||
Reference in New Issue
Block a user