ai / bt-source /panel /mod /project /docker /composeMod.py
GGSheng's picture
feat: deploy Gemma 4 to hf space
17e971c verified
# coding: utf-8
# -------------------------------------------------------------------
# 宝塔Linux面板
# -------------------------------------------------------------------
# Copyright (c) 2015-2099 宝塔软件(http://bt.cn) All rights reserved.
# -------------------------------------------------------------------
# Author: wzz <wzz@bt.cn>
# -------------------------------------------------------------------
# ------------------------------
# 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, "path参数不能为空", code=1)))
return
if not os.path.exists(get.path):
get._ws.send(
json.dumps(self.wsResult(False, "[{}] 文件不存在".format(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, " {}完成,如日志无异常再关闭此窗口!\r\n".format(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 = "更新"
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 = "重建"
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 = "停止"
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 = "启动"
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 = "停止"
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 = "添加容器编排"
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 = "重启"
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 = "获取编排列表"
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, "path参数不能为空", code=1)))
return self.wsResult(False, "path参数不能为空", code=1)
if not os.path.exists(get.path):
get._ws.send(
json.dumps(self.wsResult(False, "[{}] 文件不存在".format(get.path), code=2)))
return self.wsResult(False, "[{}] 文件不存在".format(get.path), code=1)
get.option = "获取指定编排的容器信息"
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":"提示信息"}
'''
#每个compose容器获取10行日志
self.set_tail("10")
get.option = "读取日志"
command = self.set_type(1).set_path(get.path).get_compose_logs()
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, "path参数不能为空", code=1)))
return
if not os.path.exists(get.path):
if hasattr(get, '_ws'):
get._ws.send(
json.dumps(self.wsResult(False, "[{}] 文件不存在".format(get.path), code=2)))
return
try:
config_body = public.readFile(get.path)
# env_path = 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, "获取成功", 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, "获取失败", 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, "path参数不能为空", code=1)))
return
if not os.path.exists(get.path):
if hasattr(get, '_ws'):
get._ws.send(
json.dumps(self.wsResult(False, "[{}] 文件不存在".format(get.path), code=2)))
return
if public.check_chinese(get.path):
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, "文件路径不能包含中文!", code=3)))
return
if get.config is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, "config参数不能为空", code=3)))
return
if get.env is None:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(False, "env参数不能为空", code=3)))
return
try:
stdout, stderr = self.check_config(get)
if stderr:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
"保存失败,请检查compose.yaml文件格式是否正确: 【{}】".format(stderr),
code=4,
)))
return
if "Segmentation fault" in stdout:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
"保存失败,docker-compose 版本过低,请升级到最新版!",
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,
"保存成功",
)))
return
except:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
"保存失败",
)))
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,
"project_name参数不能为空",
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,
"config参数不能为空",
code=2,
)))
return
stdout, stderr = self.check_config(get)
if stderr:
if hasattr(get, '_ws'):
get._ws.send(json.dumps(self.wsResult(
False,
"创建失败,请检查compose.yaml文件格式是否正确:\r\n{}".format(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,
"创建失败,docker-compose 版本过低,请升级到最新版!",
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 btdockerModel 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,
"template_name参数不能为空",
code=1,
)))
return
from btdockerModel 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,
"模板名称已存在,请删除模板后再添加!",
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,
"项目名已经存在,请先删除后再添加!",
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, "project_name参数不能为空", code=1)))
return
get.path = get.get("path/s", None)
if get.path is None:
get._ws.send(json.dumps(self.wsResult(False, "path参数不能为空", code=1)))
return
from btdockerModel 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,
"删除失败,请检查compose.yaml文件格式是否正确:\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,
"删除容器编排",
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, "project_list参数不能为空", 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,
"project_list参数格式错误: {}".format(project),
code=1,
)))
continue
if project["project_name"] is None or project["project_name"] == "":
get._ws.send(
json.dumps(self.wsResult(False, "project_name参数不能为空", code=1)))
continue
if project["path"] is None or project["path"] == "":
get._ws.send(json.dumps(self.wsResult(False, "path参数不能为空", code=1)))
continue
from btdockerModel 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,
"删除失败,docker-compose 版本过低,请升级到最新版!",
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.returnResult(False, "container_id参数不能为空", code=1)
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.returnResult(False, "容器不存在", code=2)
return public.returnResult(True, stdout.replace("\n", "\r\n"), code=0)
# 2024/7/18 上午10:13 修改指定项目备注
def edit_remark(self, get):
'''
@name 修改指定项目备注
'''
get.name = get.get("name", None)
get.remark = get.get("remark", "")
if get.name is None:
return public.returnResult(False, "缺少参数name!", code=2)
old_remark = ""
from btdockerModel 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.returnResult(False, "缺少参数path!", code=2)
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("项目 [{}] 的备注修改成功 [{}] --> [{}]!".format(
get.name,
old_remark,
public.xsssec(get.remark)))
return public.returnResult(True, "修改成功!")
def compose_backup_list(self, get):
'''
@name 获取 指定compose项目备份列表
@param p <int> 当前页码 默认1
@param limit <int> 每页数量 默认20
@param search <str> 搜索关键词
@param type <str> 备份类型 1:本机备份 2:上传备份
@return dict{"status":True/False,"msg":"提示信息"}
'''
page = int(get.p) if hasattr(get, 'p') else 1
limit = int(get.limit) if hasattr(get, 'limit') else 20
query = get.get("search", "")
_type = get.get("type", "")
where_str = "1=?"
where_param = [1]
if query:
where_str += "and name=?"
where_param.append(query)
if _type:
where_str += "and type=?"
where_param.append(_type)
count = public.M("compose_backup").where(where_str,where_param).count()
page_data = public.get_page(count=count,p=page,rows=limit)
backup_list = public.M("compose_backup").where(where_str,where_param).limit(page_data['row'],page_data['shift']).order('time desc').select()
page_data["data"] = backup_list
for backup in backup_list:
backup["time"] = public.format_date(times= backup["time"])
public.set_module_logs('composeBackup', 'backup_list', 1)
return page_data
def compose_backup_delete(self, get):
'''
@name 删除 compose项目备份
@param id <int> 备份ID
@return dict{"status":True/False,"msg":"提示信息"}
'''
# 1. 获取id参数
backup_id = get.get("id", None)
if backup_id is None:
return public.returnResult(False, "id参数不能为空")
# 2. 检查备份是否存在
backup_info = public.M("compose_backup").where("id=?", (backup_id,)).find()
if not backup_info:
return public.returnResult(False, "备份不存在")
# 3. 删除备份记录
public.M("compose_backup").where("id=?", (backup_id,)).delete()
# 4. 删除备份文件
public.ExecShell(f"rm -rf {backup_info['path']}")
return public.returnResult(True, "删除成功")
def compose_backup(self, get):
'''
@name 备份 compose项目
@param path <str> compose项目路径
@param name <str> 备份名称
@return dict{"status":True/False,"msg":"提示信息"}
'''
#往TMP下写入锁文件,防止重复备份
lock_file = f"/tmp/compose_backup.pl"
if os.path.exists(lock_file):
return public.returnResult(False, "当前有应用正在备份中,请等待完成后再试...")
public.writeFile(lock_file, "")
# 1. 获取path参数
path = get.get("path", None)
if path is None:
return public.returnResult(False, "path参数不能为空")
# 2. 检查path是否存在
if not os.path.exists(path):
return public.returnResult(False, "path参数不存在")
compose_name = get.get("name", "")
if not compose_name:
return public.returnResult(False, "name参数不能为空")
#检查name包含特殊字符
if "&" in compose_name or "|" in compose_name or ";" in compose_name or " " in compose_name:
return public.returnResult(False, "name参数不能包含特殊字符(& | ; 空格等)")
panel_path = public.get_panel_path()
exec_shell = '(btpython -u {panel_path}/script/docker_compose_backup.py "{compose_name}" "{path}";rm -rf {lock_file})'.format(panel_path=panel_path, compose_name=compose_name, path=path, lock_file=lock_file)
import panelTask
task_obj = panelTask.bt_task()
task_id = task_obj.create_task('compose项目备份任务', 0, exec_shell)
public.set_module_logs('composeBackup', 'backup', 1)
return {'status': True, 'msg': 'compose项目备份任务已创建.', 'task_id': task_id}
def compose_restore_config(self, get):
'''
@name 恢复 compose项目配置
@param id <int> 备份ID
@return dict{"status":True/False,"msg":"提示信息"}
'''
backup_id = get.get("id", None)
if backup_id is None:
return public.returnResult(False, "id参数不能为空")
backup_info = public.M("compose_backup").where("id=?", (backup_id,)).find()
if not backup_info:
return public.returnResult(False, "备份不存在")
file_path = backup_info.get("path", "")
if not file_path or not os.path.exists(file_path):
return public.returnResult(False, "备份文件不存在")
back_config = {}
try:
base = os.path.splitext(os.path.basename(file_path))[0]
target_path = f"{base}/config.json"
cmd = f"tar -xf {file_path} -C /tmp {target_path}"
public.ExecShell(cmd)
config = public.ReadFile(f"/tmp/{target_path}")
back_config = json.loads(config)
except Exception as e:
return public.returnResult(False, "读取备份失败: {}".format(str(e)))
if not back_config:
return public.returnResult(False, "备份配置文件读取失败...")
try:
from mod.project.docker.docker_compose.compose_utils import DockerComposeUtils
# 检查网络是否存在
nets = back_config.get("networks", [])
for net in nets:
net_name = net.get('name')
if net_name:
net["exists"] = DockerComposeUtils.network_exists(net_name)
except Exception as e:
return public.returnResult(False, "恢复配置失败: {}".format(str(e)))
return public.returnResult(True, "获取成功", back_config)
def compose_restore(self, get):
'''
@name 恢复 compose项目
@param id <int> 备份ID
@param skip_volumes <str> 不恢复的卷,多个用逗号分隔
@return dict{"status":True/False,"msg":"提示信息"}
'''
backup_id = get.get("id", None)
if backup_id is None:
return public.returnResult(False, "id参数不能为空")
lock_file = f"/tmp/compose_restore.pl"
if os.path.exists(lock_file):
return public.returnResult(False, "当前有应用正在备份中,请等待完成后再试...")
public.writeFile(lock_file, "")
backup_info = public.M("compose_backup").where("id=?", (backup_id,)).find()
if not backup_info:
return public.returnResult(False, "备份不存在")
file_path = backup_info.get("path", "")
if not file_path or not os.path.exists(file_path):
return public.returnResult(False, "备份文件不存在")
skip_volumes = get.get("skip_volumes", "")
panel_path = public.get_panel_path()
exec_shell = '(btpython -u {panel_path}/script/docker_compose_restore.py "{path}" {skip_volumes};rm -rf {lock_file})'.format(panel_path=panel_path,path=file_path, skip_volumes=skip_volumes, lock_file=lock_file)
# public.print_log(exec_shell)
import panelTask
task_obj = panelTask.bt_task()
task_id = task_obj.create_task('compose项目恢复任务', 0, exec_shell)
public.set_module_logs('composeBackup', 'restore', 1)
return {'status': True, 'msg': 'compose项目恢复任务已创建.', 'task_id': task_id}
def import_backup(self, get):
'''
@name 导入compose项目备份
@param path <str> 备份文件路径
@param ps <str> 备注信息
@return dict{"status":True/False,"msg":"提示信息"}
'''
path = get.get("path", "")
if not path:
return public.returnResult(False, "path参数不能为空")
if not os.path.exists(path):
return public.returnResult(False, "备份文件不存在")
# 检查是否已存在
# if public.M("compose_backup").where("path=?", (path,)).count() > 0:
# return public.returnResult(False, "该备份文件已存在记录中")
try:
# 读取备份配置
base = os.path.splitext(os.path.basename(path))[0]
target_path = f"{base}/config.json"
cmd = f"tar -xf {path} -C /tmp {target_path}"
public.ExecShell(cmd)
tmp_path = f"/tmp/{target_path}"
config_content = public.ReadFile(tmp_path)
if not config_content:
return public.returnResult(False, "读取备份失败: 未能提取配置文件")
config = json.loads(config_content)
if os.path.exists(tmp_path):
os.remove(tmp_path)
except Exception as e:
return public.returnResult(False, "读取备份文件失败: {}".format(str(e)))
# 准备数据库数据
try:
file_size = os.path.getsize(path)
project_name = config.get("project_name", "")
project_dir = config.get("project_dir", "")
back_time = config.get("back_time", "")
pdata = {
"type": "2", # 上传备份
"name": project_name,
"path": path,
"file_size": file_size,
"compose_path": project_dir,
"time": back_time,
"ps": get.get("ps", "手动导入")
}
public.M("compose_backup").insert(pdata)
public.set_module_logs('composeBackup', 'import_backup', 1)
return public.returnResult(True, "导入成功")
except Exception as e:
return public.returnResult(False, "导入失败: {}".format(str(e)))