Files
yakpanel-core/mod/project/docker/composeMod.py
2026-04-07 02:04:22 +05:30

748 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# coding: utf-8
# -------------------------------------------------------------------
# YakPanel
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
# -------------------------------------------------------------------
# Author: wzz <wzz@yakpanel.com>
# -------------------------------------------------------------------
# ------------------------------
# docker模型 - docker compose
# ------------------------------
import json
import os
import sys
import time
if "/www/server/panel/class" not in sys.path:
sys.path.insert(0, "/www/server/panel/class")
import public
os.chdir("/www/server/panel")
if "/www/server/panel" not in sys.path:
sys.path.insert(0, "/www/server/panel")
from mod.project.docker.docker_compose.base import Compose
# 2024/6/25 下午2:16 检查相同传参的装饰器
def check_file(func):
'''
@name 检查相同传参的装饰器
@author wzz <2024/6/25 下午2:30>
@param get.path : 传docker-compose.yaml的绝对路劲;
get.def_name : 传需要使用的函数名如get_log
@return dict{"status":True/False,"msg":"提示信息"}
'''
def wrapper(self, get, *args, **kwargs):
try:
get.path = get.get("path/s", None)
if get.path is None:
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
return
if not os.path.exists(get.path):
get._ws.send(
json.dumps(self.wsResult(False, public.lang("[{}] file does not exist",get.path), code=2)))
return
func(self, get, *args, **kwargs)
if get.def_name in ("create", "up", "update", "start", "stop", "restart","rebuild"):
get._ws.send(
json.dumps(self.wsResult(True, public.lang(" {} completed, if the log no exception to close this window!\r\n",get.option), data=-1, code=-1)))
except Exception as e:
return
return wrapper
class main(Compose):
def __init__(self):
super(main, self).__init__()
# 2024/6/25 下午2:41 执行docker-compose命令获取实时输出
def exec_cmd(self, get, command):
'''
@name 执行docker-compose命令获取实时输出
@author wzz <2024/6/25 下午2:41>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
import pty
try:
def read_output(fd, ws):
while True:
output = os.read(fd, 1024)
if not output:
break
if hasattr(get, '_ws'):
ws.send(json.dumps(self.wsResult(
True,
output.decode(),
)))
pid, fd = pty.fork()
if pid == 0:
os.execvp(command[0], command)
else:
read_output(fd, get._ws)
except:
if self.def_name in ("get_logs", "get_project_container_logs"):
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
True,
"",
)))
return
# 2024/6/25 下午2:44 更新指定docker-compose里面的镜像
@check_file
def update(self, get):
'''
@name 更新指定docker-compose里面的镜像
@param get
@return dict{"status":True/False,"msg":"提示信息"}
'''
get.option = "Update"
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
True,
"",
)))
command = self.set_type(1).set_path(get.path).get_compose_pull()
self.status_exec_logs(get, command)
command = self.set_type(1).set_path(get.path).get_compose_up_remove_orphans()
self.status_exec_logs(get, command)
# 2024/6/28 下午2:19 重建指定docker-compose项目
@check_file
def rebuild(self, get):
'''
@name 重建指定docker-compose项目
@param get
@return dict{"status":True/False,"msg":"提示信息"}
'''
get.option = "Rebuild"
command = self.set_type(1).set_path(get.path).get_compose_down()
self.status_exec_logs(get, command)
command = self.set_type(1).set_path(get.path).get_compose_up_remove_orphans()
self.status_exec_logs(get, command)
# 2024/6/24 下午10:54 停止指定docker-compose项目
@check_file
def stop(self, get):
'''
@name 停止指定docker-compose项目
@author wzz <2024/6/24 下午10:54>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
get.option = "Stop"
command = self.set_type(1).set_path(get.path).get_compose_stop()
self.status_exec_logs(get, command)
# 2024/6/24 下午10:54 启动指定docker-compose项目
@check_file
def start(self, get):
'''
@name 启动指定docker-compose项目
'''
get.option = "Start"
command = self.set_type(1).set_path(get.path).get_compose_up_remove_orphans()
self.status_exec_logs(get, command)
# 2024/6/24 下午11:23 down指定docker-compose项目
@check_file
def down(self, get):
'''
@name 停止指定docker-compose项目并删除容器、网络、镜像等
'''
get.option = "Stop"
command = self.set_type(1).set_path(get.path).get_compose_down()
self.status_exec_logs(get, command)
# 2024/6/24 下午11:23 部署指定docker-compose项目
@check_file
def up(self, get):
'''
@name 部署指定docker-compose项目
'''
get.option = "Add container orchestration"
command = self.set_type(1).set_path(get.path).get_compose_up_remove_orphans()
self.status_exec_logs(get, command)
# 2024/6/24 下午11:23 重启指定docker-compose项目
@check_file
def restart(self, get):
'''
@name 重启指定docker-compose项目
'''
get.option = "Reboot"
command = self.set_type(1).set_path(get.path).get_compose_restart()
# self.exec_logs(get, command)
self.status_exec_logs(get, command)
# 2024/6/26 下午4:28 获取docker-compose ls -a --format json
def ls(self, get):
'''
@name 获取docker-compose ls -a --format json
'''
get.option = "Get the orchestration list"
command = self.get_compose_ls()
try:
cmd_result = public.ExecShell(command)[0]
if "Segmentation fault" in cmd_result:
return []
return json.loads(cmd_result)
except:
return []
# 2024/6/26 下午8:38 获取指定compose.yaml的docker-compose ps
def ps(self, get):
'''
@name 获取指定compose.yaml的docker-compose ps
@author wzz <2024/6/26 下午8:38>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
get.path = get.get("path/s", None)
if get.path is None:
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
return self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)
if not os.path.exists(get.path):
get._ws.send(
json.dumps(self.wsResult(False, public.lang("[{}] file does not exist",get.path), code=2)))
return self.wsResult(False, public.lang("[{}] file does not exist",get.path), code=1)
get.option = "Obtain the container information of the specified orchestration"
command = self.set_path(get.path, rep=True).get_compose_ps()
try:
cmd_result = public.ExecShell(command)[0]
if "Segmentation fault" in cmd_result:
return []
if not cmd_result.startswith("["):
return json.loads("[" + cmd_result.strip().replace("\n", ",") + "]")
else:
return json.loads(cmd_result.strip().replace("\n", ","))
except:
self.ps_count += 1
if self.ps_count < 5:
time.sleep(0.5)
return self.ps(get)
return []
# 2024/6/24 下午10:53 获取指定docker-compose的运行日志
@check_file
def get_logs(self, get):
'''
@name websocket接口执行docker-compose命令返回结果执行self.get_compose_logs()命令
@param get
@return dict{"status":True/False,"msg":"提示信息"}
'''
self.set_tail("10")
get.option = "Read the logs"
command = self.set_type(1).set_path(get.path).get_compose_logs()
# public.print_log(" 获取日志 ,命令 --{}".format(command))
# public.print_log(" 获取日志 ,get --{}".format(get))
self.exec_logs(get, command)
# 2024/6/26 下午9:24 获取指定compose.yaml的内容
def get_config(self, get):
'''
@name 获取指定compose.yaml的内容
@author wzz <2024/6/26 下午9:25>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
get.path = get.get("path/s", None)
if get.path is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
return
if not os.path.exists(get.path):
if hasattr(get, '_ws'):
get._ws.send(
json.dumps(self.wsResult(False, public.lang("[{}] file does not exist",get.path), code=2)))
return
try:
config_body = public.readFile(get.path)
# env_body = public.readFile(get.path.replace("docker-compose.yaml", ".env").replace("docker-compose.yml", ".env"))
# 获取文件路径 有些情况不是用标准文件名进行启动容器的
file_path = os.path.dirname(get.path)
env_path = os.path.join(file_path, ".env")
# 判断路径下.env 文件是否存在
env_body = public.readFile(env_path) if os.path.exists(env_path) else ""
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(True, public.lang("Get ahead"), data={
"config": config_body if config_body else "",
"env": env_body if env_body else "",
})))
return
except:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("Failed to get"), data={}, code=3)))
return
# 2024/6/26 下午9:31 保存指定compose.yaml的内容
def save_config(self, get):
'''
@name 保存指定compose.yaml的内容
@param get
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
get.path = get.get("path/s", None)
get.config = get.get("config/s", None)
get.env = get.get("env/s", None)
if get.path is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
return
if not os.path.exists(get.path):
if hasattr(get, '_ws'):
get._ws.send(
json.dumps(self.wsResult(False, public.lang("[{}] file does not exist",get.path), code=2)))
return
if public.check_chinese(get.path):
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("The file path cannot contain Chinese!"), code=3)))
return
if get.config is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("The config parameter cannot be empty"), code=3)))
return
if get.env is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, public.lang("The env parameter cannot be empty"), code=3)))
return
try:
stdout, stderr = self.check_config(get)
if stderr:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("Saving failed, please check whether the compose.yaml file format is correct: 【{}",stderr),
code=4,
)))
return
if "Segmentation fault" in stdout:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("The save failed. The docker-compose version is too low. Please upgrade to the latest version!"),
code=4,
)))
return
public.writeFile(get.path, get.config)
env_path = os.path.join(os.path.dirname(get.path), ".env")
public.writeFile(env_path,get.env)
# self.up(get)
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
True,
public.lang("The save was successful"),
)))
return
except:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("Save failed"),
)))
return
# 2024/6/27 上午10:25 检查compose内容是否正确
def check_config(self, get):
'''
@name 检查compose内容是否正确
@author wzz <2024/6/27 上午10:26>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
if not os.path.exists("/tmp/btdk"):
os.makedirs("/tmp/btdk", 0o755, True)
tmp_path = "/tmp/btdk/{}".format(os.path.basename(public.GetRandomString(10).lower()))
public.writeFile(tmp_path, get.config)
public.writeFile("/tmp/btdk/.env", get.env)
command = self.set_path(tmp_path, rep=True).get_compose_config()
stdout, stderr = public.ExecShell(command)
if "`version` is obsolete" in stderr:
public.ExecShell("sed -i '/version/d' {}".format(tmp_path))
get.config = public.readFile(tmp_path)
return self.check_config(get)
public.ExecShell("rm -f {}".format(tmp_path))
return stdout, stderr
# 2024/6/27 上午10:06 根据内容创建docker-compose编排
def create(self, get):
'''
@name 根据内容创建docker-compose编排
@author wzz <2024/6/27 上午10:07>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
get.project_name = get.get("project_name/s", None)
if get.project_name is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("The project_name parameter cannot be empty"),
code=1,
)))
return
get.config = get.get("config/s", None)
if get.config is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("The config parameter cannot be empty"),
code=2,
)))
return
stdout, stderr = self.check_config(get)
if stderr:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("Creation failed, please check whether the compose.yaml file format is correct: \r\n{}",stderr.replace("\n", "\r\n")),
code=4,
)))
return
if "Segmentation fault" in stdout:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("Creation failed, the docker-compose version is too low, please upgrade to the latest version!"),
code=4,
)))
return
# 2024/2/20 下午 3:21 如果检测到是中文的compose则自动转换为英文
config_path = "{}/config/name_map.json".format(public.get_panel_path())
try:
name_map = json.loads(public.readFile(config_path))
import re
if re.findall(r"[\u4e00-\u9fa5]", get.project_name):
name_str = 'bt_compose_' + public.GetRandomString(10).lower()
name_map[name_str] = get.project_name
get.project_name = name_str
public.writeFile(config_path, json.dumps(name_map))
except:
pass
if not os.path.exists(self.compose_project_path): os.makedirs(self.compose_project_path, 0o755, True)
if not os.path.exists(os.path.join(self.compose_project_path, get.project_name)):
os.makedirs(os.path.join(self.compose_project_path, get.project_name), 0o755, True)
get.path = os.path.join(self.compose_project_path, "{}/docker-compose.yaml".format(get.project_name))
public.writeFile(get.path, get.config)
public.writeFile(get.path.replace("docker-compose.yaml", ".env").replace("docker-compose.yml", ".env"), get.env)
get.add_template = get.get("add_template/d", 0)
template_id = None
from btdockerModelV2 import dk_public as dp
if get.add_template == 1:
get.template_name = get.get("template_name/s", None)
if get.template_name is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("template_name parameter cannot be empty"),
code=1,
)))
return
from btdockerModelV2 import composeModel as cm
template_list = cm.main()._template_list(get)
for template in template_list:
if get.template_name == template['name']:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("The template name already exists, please delete the template and add it again!"),
code=2,
)))
return
#添加编排模板 ---------- 可以直接引用composeModel.add_template
template_path = os.path.join(self.compose_project_path, "{}".format(get.template_name))
compose_path = os.path.join(template_path,"docker-compose.yaml")
env_path = os.path.join(template_path,".env")
pdata = {
"name": get.template_name,
"remark": "",
"path": template_path,
"add_in_path":1
}
template_id = dp.sql("templates").insert(pdata)
if not os.path.exists(template_path):
os.makedirs(template_path, 0o755, True)
public.writeFile(compose_path, get.config)
public.writeFile(env_path,get.env)
get.remark = get.get("remark/s", "")
stacks_info = dp.sql("stacks").where("name=?", (public.xsssec(get.project_name))).find()
if not stacks_info:
pdata = {
"name": public.xsssec(get.project_name),
"status": "1",
"path": get.path,
"template_id": template_id,
"time": time.time(),
"remark": public.xsssec(get.remark)
}
dp.sql("stacks").insert(pdata)
else:
check_status = public.ExecShell("docker-compose ls |grep {}".format(get.path))[0]
if not check_status:
dp.sql("stacks").where("name=?", (public.xsssec(get.project_name))).delete()
else:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("The project name already exists, please delete it before adding it!"),
code=3,
)))
return
self.up(get)
# 2024/6/27 上午11:42 删除指定compose.yaml的docker-compose编排
def delete(self, get):
'''
@name 删除指定compose.yaml的docker-compose编排
@author wzz <2024/6/27 上午11:42>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
get.project_name = get.get("project_name/s", None)
if get.project_name is None:
get._ws.send(json.dumps(self.wsResult(False, public.lang("The project_name parameter cannot be empty"), code=1)))
return
get.path = get.get("path/s", None)
if get.path is None:
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
return
from btdockerModelV2 import dk_public as dp
stacks_info = dp.sql("stacks").where("path=? or name=?", (get.path, get.project_name)).find()
if stacks_info:
dp.sql("stacks").where("path=? or name=?", (get.path, get.project_name)).delete()
if "bt_compose_" in get.path:
config_path = "{}/config/name_map.json".format(public.get_panel_path())
name_map = json.loads(public.readFile(config_path))
bt_compose_name = os.path.dirname(get.path).split("/")[-1]
if bt_compose_name in name_map:
name_map.pop(bt_compose_name)
public.writeFile(config_path, json.dumps(name_map))
stacks_list = dp.sql("stacks").select()
compose_list = self.ls(get)
for i in stacks_list:
for j in compose_list:
if i['name'] == j['Name']:
break
if public.md5(i['name']) in j['Name']:
break
else:
dp.sql("stacks").where("name=?", (i['name'])).delete()
if not os.path.exists(get.path):
command = self.set_type(0).set_compose_name(get.project_name).get_compose_delete_for_ps()
else:
command = self.set_type(0).set_path(get.path).get_compose_delete()
stdout, stderr = public.ExecShell(command)
if "invalid compose project" in stderr:
command = self.set_type(0).set_compose_name(get.project_name).get_compose_delete_for_ps()
stdout, stderr = public.ExecShell(command)
if stderr and "Error" in stderr:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
"Removal fails, check if the compose.yaml file format is correct\r\n{}".format(stderr.replace("\n", "\r\n")),
data=-1,
code=4,
)))
return
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
True,
public.lang("Delete container orchestration"),
data=-1,
code=0
)))
# 2024/6/27 下午8:39 批量删除指定compose.yaml的docker-compose编排
def batch_delete(self, get):
'''
@name 批量删除指定compose.yaml的docker-compose编排
@param get
@return dict{"status":True/False,"msg":"提示信息"}
'''
if self.def_name is None: self.set_def_name(get.def_name)
get.project_list = get.get("project_list", None)
if get.project_list is None or len(get.project_list) == 0:
return self.wsResult(False, public.lang("The project_list parameter cannot be empty"), code=1)
config_path = "{}/config/name_map.json".format(public.get_panel_path())
try:
name_map = json.loads(public.readFile(config_path))
except:
name_map = {}
for project in get.project_list:
if not isinstance(project, dict):
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("project_list parameter format error: {}",project),
code=1,
)))
continue
if project["project_name"] is None or project["project_name"] == "":
get._ws.send(
json.dumps(self.wsResult(False, public.lang("The project_name parameter cannot be empty"), code=1)))
continue
if project["path"] is None or project["path"] == "":
get._ws.send(json.dumps(self.wsResult(False, public.lang("The path parameter cannot be empty"), code=1)))
continue
from btdockerModelV2 import dk_public as dp
stacks_info = dp.sql("stacks").where("path=? or name=?", (project["path"], project["project_name"])).find()
if stacks_info:
dp.sql("stacks").where("path=? or name=?", (project["path"], project["project_name"])).delete()
if "bt_compose_" in project["path"]:
bt_compose_name = os.path.dirname(project["path"]).split("/")[-1]
if bt_compose_name in name_map:
name_map.pop(bt_compose_name)
if not os.path.exists(project["path"]):
command = self.set_type(0).set_compose_name(project["project_name"]).get_compose_delete_for_ps()
else:
command = self.set_type(0).set_path(project["path"], rep=True).get_compose_delete()
stdout, stderr = public.ExecShell(command)
if "Segmentation fault" in stdout:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
public.lang("Deletion failed, docker-compose version is too low, please upgrade to the latest version!"),
code=4,
)))
return
# public.ExecShell("rm -rf {}".format(os.path.dirname(project["path"])))
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
True,
data={
"project_name": project["project_name"],
"status": True
}
)))
public.writeFile(config_path, json.dumps(name_map))
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(True, data=-1)))
# 2024/6/28 下午3:15 根据容器id获取指定容器的日志
def get_project_container_logs(self, get):
'''
@name 根据容器id获取指定容器的日志
@author wzz <2024/6/28 下午3:16>
@param "data":{"参数名":""} <数据类型> 参数描述
@return dict{"status":True/False,"msg":"提示信息"}
'''
get.container_id = get.get("container_id/s", None)
if get.container_id is None:
return public.return_message(-1, 0, public.lang("The container_id parameter cannot be empty"))
self.set_tail("200")
self.set_container_id(get.container_id)
command = self.get_container_logs()
stdout, stderr = public.ExecShell(command)
if "invalid compose project" in stderr:
return public.return_message(-1, 0, public.lang("The container does not exist"))
return public.return_message(0, 0, stdout.replace("\n", "\r\n"))
# 2024/7/18 上午10:13 修改指定项目备注
def edit_remark(self, get):
'''
@name 修改指定项目备注
'''
try:
get.name = get.get("name", None)
get.remark = get.get("remark", "")
if get.name is None:
return public.return_message(-1, 0, public.lang("Please pass the name parameter!"))
old_remark = ""
from btdockerModelV2 import dk_public as dp
stacks_info = dp.sql("stacks").where("name=?", (public.xsssec(get.name))).find()
if not stacks_info:
get.path = get.get("path", None)
if get.path is None:
return public.return_message(-1, 0, public.lang("Please pass the path parameter!"))
pdata = {
"name": public.xsssec(get.name),
"status": "1",
"path": get.path,
"template_id": None,
"time": time.time(),
"remark": public.xsssec(get.remark)
}
dp.sql("stacks").insert(pdata)
else:
old_remark = stacks_info['remark']
dp.sql("stacks").where("name=?", (public.xsssec(get.name))).update({"remark": public.xsssec(get.remark)})
dp.write_log("Comments for project [{}] changed successfully [{}] --> [{}]!".format(
get.name,
old_remark,
public.xsssec(get.remark)))
return public.return_message(0, 0, public.lang("Modify successfully!"))
except:
public.print_log(public.get_error_info())