398 lines
16 KiB
Python
398 lines
16 KiB
Python
|
|
# coding: utf-8
|
|||
|
|
# -------------------------------------------------------------------
|
|||
|
|
# YakPanel
|
|||
|
|
# -------------------------------------------------------------------
|
|||
|
|
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
|||
|
|
# -------------------------------------------------------------------
|
|||
|
|
# Author: wzz <wzz@yakpanel.com>
|
|||
|
|
# -------------------------------------------------------------------
|
|||
|
|
# ------------------------------
|
|||
|
|
# docker模型
|
|||
|
|
# ------------------------------
|
|||
|
|
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")
|
|||
|
|
|
|||
|
|
os.chdir("/www/server/panel")
|
|||
|
|
import public
|
|||
|
|
|
|||
|
|
from mod.project.docker.app.appManageMod import AppManage
|
|||
|
|
# from mod.project.docker.runtime.runtimeManage import RuntimeManage
|
|||
|
|
# from mod.project.docker.sites.sitesManage import SitesManage
|
|||
|
|
from mod.project.docker.app.sub_app.ollamaMod import OllamaMod
|
|||
|
|
from mod.project.docker.apphub.apphubManage import AppHub
|
|||
|
|
from btdockerModelV2 import dk_public as dp
|
|||
|
|
|
|||
|
|
|
|||
|
|
class main(AppManage, OllamaMod):
|
|||
|
|
|
|||
|
|
def __init__(self):
|
|||
|
|
super(main, self).__init__()
|
|||
|
|
OllamaMod.__init__(self)
|
|||
|
|
|
|||
|
|
# 2024/6/26 下午5:49 获取所有已部署的项目列表
|
|||
|
|
def get_project_list(self, get):
|
|||
|
|
'''
|
|||
|
|
@name 获取所有已部署的项目列表
|
|||
|
|
@author wzz <2024/6/26 下午5:49>
|
|||
|
|
@param "data":{"参数名":""} <数据类型> 参数描述
|
|||
|
|
@return dict{"status":True/False,"msg":"提示信息"}
|
|||
|
|
'''
|
|||
|
|
try:
|
|||
|
|
if self.def_name is None: self.set_def_name(get.def_name)
|
|||
|
|
if hasattr(get, '_ws') and hasattr(get._ws, 'btws_get_project_list'):
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
compose_list = self.ls(get)
|
|||
|
|
if len(compose_list) == 0:
|
|||
|
|
if hasattr(get, '_ws'):
|
|||
|
|
get._ws.send(json.dumps(self.wsResult(
|
|||
|
|
True,
|
|||
|
|
data=[],
|
|||
|
|
)))
|
|||
|
|
|
|||
|
|
|
|||
|
|
stacks_info = dp.sql("stacks").select()
|
|||
|
|
|
|||
|
|
compose_project = []
|
|||
|
|
|
|||
|
|
for j in compose_list:
|
|||
|
|
t_status = j["Status"].split(",")
|
|||
|
|
container_count = 0
|
|||
|
|
for ts in t_status:
|
|||
|
|
container_count += int(ts.split("(")[1].split(")")[0])
|
|||
|
|
|
|||
|
|
j_name = j['Name']
|
|||
|
|
if "bt_compose_" in j_name:
|
|||
|
|
config_path = "{}/config/name_map.json".format(public.get_panel_path())
|
|||
|
|
name_map = json.loads(public.readFile(config_path))
|
|||
|
|
if j_name in name_map:
|
|||
|
|
j_name = name_map[j_name]
|
|||
|
|
else:
|
|||
|
|
j_name = j_name.replace("bt_compose_", "")
|
|||
|
|
|
|||
|
|
tmp = {
|
|||
|
|
"id": None,
|
|||
|
|
"name": j_name,
|
|||
|
|
"status": "1",
|
|||
|
|
"path": j['ConfigFiles'],
|
|||
|
|
"template_id": None,
|
|||
|
|
"time": None,
|
|||
|
|
"remark": "",
|
|||
|
|
"run_status": j['Status'].split("(")[0].lower(),
|
|||
|
|
"container_count": container_count,
|
|||
|
|
}
|
|||
|
|
for i in stacks_info:
|
|||
|
|
if public.md5(i['name']) in j['Name']:
|
|||
|
|
|
|||
|
|
tmp["name"] = i['name']
|
|||
|
|
tmp["run_status"] = j['Status'].split("(")[0].lower()
|
|||
|
|
tmp["template_id"] = i['template_id']
|
|||
|
|
tmp["time"] = i['time']
|
|||
|
|
tmp["remark"] = i["remark"]
|
|||
|
|
tmp["id"] = i['id']
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if i['name'] == j['Name']:
|
|||
|
|
tmp["run_status"] = j['Status'].split("(")[0].lower()
|
|||
|
|
tmp["template_id"] = i['template_id']
|
|||
|
|
tmp["time"] = i['time']
|
|||
|
|
tmp["remark"] = i["remark"]
|
|||
|
|
tmp["id"] = i['id']
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if tmp["time"] is None:
|
|||
|
|
if os.path.exists(j['ConfigFiles']):
|
|||
|
|
get.path = j['ConfigFiles']
|
|||
|
|
compose_ps = self.ps(get)
|
|||
|
|
if len(compose_ps) > 0 and "CreatedAt" in compose_ps[0]:
|
|||
|
|
tmp["time"] = dp.convert_timezone_str_to_timestamp(compose_ps[0]['CreatedAt'])
|
|||
|
|
|
|||
|
|
compose_project.append(tmp)
|
|||
|
|
|
|||
|
|
if hasattr(get, '_ws'):
|
|||
|
|
setattr(get._ws, 'btws_get_project_list', True)
|
|||
|
|
get._ws.send(json.dumps(self.wsResult(
|
|||
|
|
True,
|
|||
|
|
data=sorted(compose_project, key=lambda x: x["time"] if x["time"] is not None else float('-inf'), reverse=True),
|
|||
|
|
)))
|
|||
|
|
|
|||
|
|
time.sleep(2)
|
|||
|
|
except Exception as e:
|
|||
|
|
return public.return_message(-1, 0, str(e))
|
|||
|
|
|
|||
|
|
# 2026/2/26 下午8:55 获取指定compose.yml的docker-compose ps 增加异常处理和类型检查,防止WebSocket推送过程中出现错误导致循环崩溃
|
|||
|
|
def get_project_ps(self, get):
|
|||
|
|
'''
|
|||
|
|
@name 获取指定 compose.yml 的 docker-compose ps 实时状态(WebSocket 推送)
|
|||
|
|
@author wzz <2026/2/26>
|
|||
|
|
@param get.path: compose 项目路径
|
|||
|
|
@return dict {"status": True/False, "msg": "..."}
|
|||
|
|
'''
|
|||
|
|
try:
|
|||
|
|
if self.def_name is None:
|
|||
|
|
self.set_def_name(get.def_name)
|
|||
|
|
|
|||
|
|
# 确保 path 存在
|
|||
|
|
if not hasattr(get, 'path') or not get.path:
|
|||
|
|
return public.return_message(-1, 0, "Missing 'path' parameter")
|
|||
|
|
|
|||
|
|
ws_flag_attr = f'btws_get_project_ps_{get.path}'
|
|||
|
|
|
|||
|
|
# 防止重复订阅
|
|||
|
|
if hasattr(get, '_ws') and hasattr(get._ws, ws_flag_attr):
|
|||
|
|
result = self.wsResult(True, data=[])
|
|||
|
|
try:
|
|||
|
|
get._ws.send(json.dumps(result))
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
return result
|
|||
|
|
|
|||
|
|
from btdockerModelV2.dockerSock import container
|
|||
|
|
sk_container = container.dockerContainer()
|
|||
|
|
|
|||
|
|
while True:
|
|||
|
|
try:
|
|||
|
|
compose_list = self.ps(get)
|
|||
|
|
# 强制确保是 list
|
|||
|
|
if not isinstance(compose_list, list):
|
|||
|
|
compose_list = []
|
|||
|
|
|
|||
|
|
# 发送空结果并退出(无容器)
|
|||
|
|
if len(compose_list) == 0:
|
|||
|
|
if hasattr(get, '_ws'):
|
|||
|
|
try:
|
|||
|
|
get._ws.send(json.dumps(self.wsResult(True, data=[])))
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# 处理每个容器
|
|||
|
|
for l in compose_list:
|
|||
|
|
if not isinstance(l, dict):
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 补全 Image
|
|||
|
|
if "Image" not in l:
|
|||
|
|
l["Image"] = ""
|
|||
|
|
if l.get("ID"):
|
|||
|
|
try:
|
|||
|
|
inspect = sk_container.get_container_inspect(l["ID"])
|
|||
|
|
l["Image"] = inspect.get("Config", {}).get("Image", "")
|
|||
|
|
except:
|
|||
|
|
pass # 忽略 inspect 失败
|
|||
|
|
|
|||
|
|
# 补全 Ports 字符串
|
|||
|
|
if "Ports" not in l:
|
|||
|
|
l["Ports"] = ""
|
|||
|
|
publishers = l.get("Publishers")
|
|||
|
|
if publishers and isinstance(publishers, list):
|
|||
|
|
for p in publishers:
|
|||
|
|
if not isinstance(p, dict):
|
|||
|
|
continue
|
|||
|
|
url = p.get("URL", "")
|
|||
|
|
target = p.get("TargetPort", "")
|
|||
|
|
proto = p.get("Protocol", "")
|
|||
|
|
pub_port = p.get("PublishedPort", "")
|
|||
|
|
if url == "":
|
|||
|
|
l["Ports"] += f"{target}/{proto},"
|
|||
|
|
else:
|
|||
|
|
l["Ports"] += f"{url}:{pub_port}->{target}/{proto},"
|
|||
|
|
|
|||
|
|
# 构造结构化 ports(兼容 containerModel.struct_container_ports)
|
|||
|
|
ports_data = {}
|
|||
|
|
publishers = l.get("Publishers")
|
|||
|
|
if publishers and isinstance(publishers, list):
|
|||
|
|
for port in publishers:
|
|||
|
|
if not isinstance(port, dict):
|
|||
|
|
continue
|
|||
|
|
key = f"{port.get('TargetPort', '')}/{port.get('Protocol', '')}"
|
|||
|
|
host_ip = port.get("URL", "")
|
|||
|
|
host_port = str(port.get("PublishedPort", ""))
|
|||
|
|
entry = {"HostIp": host_ip, "HostPort": host_port}
|
|||
|
|
if key not in ports_data:
|
|||
|
|
ports_data[key] = [entry] if host_ip else None
|
|||
|
|
elif ports_data[key] is not None:
|
|||
|
|
ports_data[key].append(entry)
|
|||
|
|
l["ports"] = ports_data
|
|||
|
|
|
|||
|
|
# 推送数据
|
|||
|
|
if hasattr(get, '_ws'):
|
|||
|
|
setattr(get._ws, ws_flag_attr, True)
|
|||
|
|
try:
|
|||
|
|
get._ws.send(json.dumps(self.wsResult(True, data=compose_list)))
|
|||
|
|
except:
|
|||
|
|
# WebSocket 已断开,退出循环
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
time.sleep(2)
|
|||
|
|
|
|||
|
|
except Exception:
|
|||
|
|
# 内部循环异常,安全退出
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
return self.wsResult(True, data=[])
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
return public.return_message(-1, 0, str(e))
|
|||
|
|
|
|||
|
|
# 2024/11/11 14:34 获取所有正在运行的容器信息和已安装的应用信息
|
|||
|
|
def get_some_info(self, get):
|
|||
|
|
'''
|
|||
|
|
@name 获取所有正在运行的容器信息和已安装的应用信息
|
|||
|
|
'''
|
|||
|
|
get.type = get.get("type", "container")
|
|||
|
|
if not get.type in ("container", "app"):
|
|||
|
|
return public.return_message(-1, 0, public.lang("Only container and app types are supported"))
|
|||
|
|
|
|||
|
|
if get.type == "container":
|
|||
|
|
from btdockerModelV2.dockerSock import container
|
|||
|
|
sk_container = container.dockerContainer()
|
|||
|
|
sk_container_list = sk_container.get_container()
|
|||
|
|
|
|||
|
|
data = []
|
|||
|
|
for container in sk_container_list:
|
|||
|
|
if not "running" in container["State"]: continue
|
|||
|
|
|
|||
|
|
port_list = []
|
|||
|
|
for p in container["Ports"]:
|
|||
|
|
if not "PublicPort" in p: continue
|
|||
|
|
if not p["PublicPort"] in port_list:
|
|||
|
|
port_list.append(p["PublicPort"])
|
|||
|
|
|
|||
|
|
data.append({
|
|||
|
|
"id": container["Id"],
|
|||
|
|
"name": container["Names"][0].replace("/", ""),
|
|||
|
|
"status": container["State"],
|
|||
|
|
"image": container["Image"],
|
|||
|
|
"created_time": container["Created"],
|
|||
|
|
"ports": port_list,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return public.return_message(0, 0, data)
|
|||
|
|
else:
|
|||
|
|
get.row = 10000
|
|||
|
|
installed_apps = self.get_installed_apps(get)['message']
|
|||
|
|
not_allow_category = ("Database", "System")
|
|||
|
|
if installed_apps and installed_apps.get('data', []):
|
|||
|
|
for app in installed_apps["data"]:
|
|||
|
|
if not "running" in app["status"]:
|
|||
|
|
installed_apps["data"].remove(app)
|
|||
|
|
if app["apptype"] in not_allow_category:
|
|||
|
|
installed_apps["data"].remove(app) if app in installed_apps["data"] else None
|
|||
|
|
|
|||
|
|
# return public.returnResult(status=installed_apps["status"], data=installed_apps["data"])
|
|||
|
|
return public.return_message(0, 0, installed_apps)
|
|||
|
|
|
|||
|
|
def generate_apphub(self, get):
|
|||
|
|
'''
|
|||
|
|
@name 解析外部应用列表
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@return dict{"status":True/False,"msg":"提示信息"}
|
|||
|
|
'''
|
|||
|
|
return AppHub().generate_apphub(get)
|
|||
|
|
|
|||
|
|
def create_app(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 创建应用
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@return dict{"status":True/False,"msg":"提示信息"}
|
|||
|
|
'''
|
|||
|
|
if get.get("appid","0") == "-1": # 从apphub创建应用
|
|||
|
|
self.templates_path = os.path.join(AppHub.hub_home_path, "templates")
|
|||
|
|
self.apps_json_file = os.path.join(AppHub.hub_home_path, "apps.json")
|
|||
|
|
|
|||
|
|
version = get.get("version","latest")
|
|||
|
|
app_name = get.get("app_name","")
|
|||
|
|
|
|||
|
|
if not os.path.exists(self.templates_path):
|
|||
|
|
os.makedirs(self.templates_path)
|
|||
|
|
|
|||
|
|
#/www/dk_project/dk_app/apphub/apphub/templates/app_name/version
|
|||
|
|
app_version_path = os.path.join(AppHub.hub_home_path, app_name, version)
|
|||
|
|
if not os.path.exists(app_version_path):
|
|||
|
|
return public.return_message(-1, 0, public.lang("Version {} for applying {} does not exist", (version, app_name)))
|
|||
|
|
|
|||
|
|
# /www/dk_project/dk_app/apphub/apphub/templates/app_name
|
|||
|
|
app_template_path = os.path.join(self.templates_path, app_name)
|
|||
|
|
|
|||
|
|
public.ExecShell("\cp -r {} {}".format(app_version_path,app_template_path))
|
|||
|
|
|
|||
|
|
return super().create_app(get)
|
|||
|
|
|
|||
|
|
def get_apphub_config(self, get):
|
|||
|
|
'''
|
|||
|
|
@name 获取apphub配置
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@return dict{"status":True/False,"data":{}}
|
|||
|
|
'''
|
|||
|
|
return public.return_message(0, 0, AppHub.get_config())
|
|||
|
|
|
|||
|
|
def set_apphub_git(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 设置外部应用的git地址
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@param get: git_url, git_branch, user, password
|
|||
|
|
'''
|
|||
|
|
if not hasattr(get, 'git_url') or not get.git_url:
|
|||
|
|
return public.return_message(-1, 0, public.lang("GIT ADDRESS IS NOT SET"))
|
|||
|
|
if not hasattr(get, 'git_branch') or not get.git_branch:
|
|||
|
|
return public.return_message(-1, 0, public.lang("The branch name is not set"))
|
|||
|
|
|
|||
|
|
return AppHub().set_apphub_git(get)
|
|||
|
|
|
|||
|
|
def import_git_apphub(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 从git导入外部应用
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
'''
|
|||
|
|
return AppHub().import_git_apphub(get)
|
|||
|
|
|
|||
|
|
def install_apphub(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 安装apphub所需环境
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
'''
|
|||
|
|
return AppHub().install_apphub(get)
|
|||
|
|
|
|||
|
|
def import_zip_apphub(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 从zip包导入外部应用
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@param get: sfile: zip文件路径
|
|||
|
|
'''
|
|||
|
|
if not hasattr(get, 'sfile') or not get.sfile:
|
|||
|
|
return public.return_message(-1, 0, public.lang("The zip file path is not set"))
|
|||
|
|
|
|||
|
|
return AppHub().import_zip_apphub(get)
|
|||
|
|
|
|||
|
|
def parser_zip_apphub(self,get):
|
|||
|
|
'''
|
|||
|
|
@name 解析zip包
|
|||
|
|
@author csj <2025/7/9>
|
|||
|
|
@param get: sfile: zip文件路径
|
|||
|
|
@return dict{"status":True/False,"data":[]}
|
|||
|
|
'''
|
|||
|
|
if not hasattr(get, 'sfile') or not get.sfile:
|
|||
|
|
return public.return_message(-1, 0, public.lang("Please select the file path"))
|
|||
|
|
|
|||
|
|
app_list = []
|
|||
|
|
files = get.sfile.split(',')
|
|||
|
|
for sfile in files:
|
|||
|
|
get.sfile = sfile
|
|||
|
|
|
|||
|
|
apps = AppHub().parser_zip_apphub(get)
|
|||
|
|
app_list.extend(apps)
|
|||
|
|
public.print_log(app_list)
|
|||
|
|
return public.return_message(0, 0, app_list)
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
pass
|