Initial YakPanel commit
This commit is contained in:
0
mod/project/docker/__init__.py
Normal file
0
mod/project/docker/__init__.py
Normal file
0
mod/project/docker/app/__init__.py
Normal file
0
mod/project/docker/app/__init__.py
Normal file
1784
mod/project/docker/app/appManageMod.py
Normal file
1784
mod/project/docker/app/appManageMod.py
Normal file
File diff suppressed because it is too large
Load Diff
1159
mod/project/docker/app/base.py
Normal file
1159
mod/project/docker/app/base.py
Normal file
File diff suppressed because it is too large
Load Diff
23
mod/project/docker/app/gpu/__init__.py
Normal file
23
mod/project/docker/app/gpu/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from typing import List
|
||||
|
||||
from .base import GPUBase
|
||||
from .nvidia import NVIDIA
|
||||
from .amd import AMD
|
||||
|
||||
class Driver:
|
||||
drivers: List[GPUBase] = []
|
||||
|
||||
def __init__(self):
|
||||
if NVIDIA.is_support():
|
||||
self.drivers.append(NVIDIA())
|
||||
|
||||
if AMD.is_support():
|
||||
self.drivers.append(AMD())
|
||||
|
||||
@property
|
||||
def support(self):
|
||||
return len(self.drivers) > 0
|
||||
|
||||
def get_all_device_info(self, get):
|
||||
for _driver in self.drivers:
|
||||
pass
|
||||
36
mod/project/docker/app/gpu/amd.py
Normal file
36
mod/project/docker/app/gpu/amd.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from mod.project.docker.app.gpu.base import GPUBase
|
||||
|
||||
class AMD(GPUBase):
|
||||
@classmethod
|
||||
def is_support(cls):
|
||||
pass
|
||||
|
||||
def _get_device_version(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def _get_device_name(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def _get_fan_info(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def main(self):
|
||||
pass
|
||||
|
||||
def get_info(self, gpu_id=0):
|
||||
pass
|
||||
|
||||
def _get_mem_info(self):
|
||||
pass
|
||||
|
||||
def _get_clock_info(self):
|
||||
pass
|
||||
|
||||
def _get_temp_info(self):
|
||||
pass
|
||||
|
||||
def _get_uti_info(self):
|
||||
pass
|
||||
|
||||
def _get_proc_uti(self, proc_name='', proc_pid=0):
|
||||
pass
|
||||
70
mod/project/docker/app/gpu/base.py
Normal file
70
mod/project/docker/app/gpu/base.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class GPUBase(ABC):
|
||||
name = 'base'
|
||||
support = None
|
||||
@abstractmethod
|
||||
def _get_mem_info(self, *args, **kwargs):
|
||||
"""
|
||||
获取显存占用
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_clock_info(self, *args, **kwargs):
|
||||
"""
|
||||
获取时钟信息
|
||||
Returns:
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_temp_info(self, *args, **kwargs):
|
||||
"""
|
||||
获取温度
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_uti_info(self, *args, **kwargs):
|
||||
"""
|
||||
获取占用
|
||||
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_proc_uti(self, *args, **kwargs):
|
||||
"""
|
||||
获取进程占用
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_fan_info(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_device_name(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_device_version(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def is_support(cls):
|
||||
pass
|
||||
27
mod/project/docker/app/gpu/constants.py
Normal file
27
mod/project/docker/app/gpu/constants.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class CMD:
|
||||
@dataclass
|
||||
class CTK:
|
||||
@dataclass
|
||||
class APT:
|
||||
GetGPGKey = "curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg"
|
||||
AddSourcesList = "curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list"
|
||||
APTUpdate = "sudo apt-get update"
|
||||
Install = "sudo apt-get install -y nvidia-container-toolkit"
|
||||
OneInstall = GetGPGKey + ';' + AddSourcesList + ';' + APTUpdate + ';' + Install
|
||||
|
||||
@dataclass
|
||||
class YUM:
|
||||
AddRepo = "curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo"
|
||||
Install = "sudo yum install -y nvidia-container-toolkit"
|
||||
OneInstall = AddRepo + ';' + Install
|
||||
|
||||
@dataclass
|
||||
class ConfigureDocker:
|
||||
Runtime = "sudo nvidia-ctk runtime configure --runtime=docker"
|
||||
Restart = "sudo systemctl restart docker"
|
||||
|
||||
CheckVersion = "nvidia-ctk -v"
|
||||
199
mod/project/docker/app/gpu/nvidia.py
Normal file
199
mod/project/docker/app/gpu/nvidia.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.append('/www/server/panel/class')
|
||||
|
||||
import public
|
||||
|
||||
try:
|
||||
import pynvml
|
||||
except:
|
||||
public.ExecShell("btpip install nvidia-ml-py")
|
||||
import pynvml
|
||||
|
||||
try:
|
||||
from mod.project.docker.app.gpu.base import GPUBase
|
||||
except:
|
||||
class GPUBase:
|
||||
pass
|
||||
|
||||
device_tasks = defaultdict()
|
||||
system_tasks = defaultdict()
|
||||
|
||||
|
||||
def register_task(name: str):
|
||||
def task_decorator(task_func):
|
||||
_task_type, _task_name = name.split(':')
|
||||
if _task_type == 'device':
|
||||
device_tasks[_task_name] = task_func
|
||||
elif _task_type == 'system':
|
||||
system_tasks[_task_name] = task_func
|
||||
|
||||
@wraps(task_func)
|
||||
def func_wrapper(*args, **kwargs):
|
||||
return task_func(*args, **kwargs)
|
||||
|
||||
return func_wrapper
|
||||
|
||||
return task_decorator
|
||||
|
||||
|
||||
class NVIDIA(GPUBase):
|
||||
name = 'nvidia'
|
||||
support = None
|
||||
|
||||
def __init__(self):
|
||||
# 判断是否支持,并在判断时初始化pynvml库。
|
||||
self.device_count = 0
|
||||
if self.is_support():
|
||||
self.device_count = pynvml.nvmlDeviceGetCount()
|
||||
|
||||
def __del__(self):
|
||||
if self.is_support():
|
||||
pynvml.nvmlShutdown()
|
||||
|
||||
def get_all_device_info(self):
|
||||
all_info = defaultdict()
|
||||
all_info['system'] = self.get_system_info()
|
||||
for index in range(self.device_count):
|
||||
all_info[index] = self.get_info_by_index(index)
|
||||
return all_info
|
||||
|
||||
def get_info_by_index(self, index=0):
|
||||
info = defaultdict()
|
||||
handle = pynvml.nvmlDeviceGetHandleByIndex(index)
|
||||
|
||||
for t_name, t_func in device_tasks.items():
|
||||
try:
|
||||
info[t_name] = t_func(self, handle)
|
||||
except:
|
||||
# public.print_log("pynvml {t_name} error: {}")
|
||||
info[t_name] = None
|
||||
|
||||
return info
|
||||
|
||||
def get_system_info(self):
|
||||
info = defaultdict()
|
||||
for t_name, t_func in system_tasks.items():
|
||||
try:
|
||||
info[t_name] = t_func(self)
|
||||
except:
|
||||
# public.print_log(f"pynvml {t_name} error: {e}")
|
||||
info[t_name] = None
|
||||
return info
|
||||
|
||||
@classmethod
|
||||
def is_support(cls):
|
||||
try:
|
||||
pynvml.nvmlInit()
|
||||
cls.support = True
|
||||
return True
|
||||
|
||||
except pynvml.NVMLError:
|
||||
cls.support = False
|
||||
# public.print_log("Nvidia was not supported!")
|
||||
return False
|
||||
|
||||
@register_task('device:memory')
|
||||
def _get_mem_info(self, handle):
|
||||
info = defaultdict()
|
||||
info['size'] = int(pynvml.nvmlDeviceGetMemoryInfo(handle).total) / 1024 ** 3
|
||||
info['free'] = int(pynvml.nvmlDeviceGetMemoryInfo(handle).free) / 1024 ** 3
|
||||
info['used'] = int(pynvml.nvmlDeviceGetMemoryInfo(handle).used) / 1024 ** 3
|
||||
return info
|
||||
|
||||
@register_task('device:clock')
|
||||
def _get_clock_info(self, handle):
|
||||
info = defaultdict()
|
||||
info['graphics'] = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_GRAPHICS)
|
||||
info['sm'] = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_SM)
|
||||
info['memory'] = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_MEM)
|
||||
info['video'] = pynvml.nvmlDeviceGetClockInfo(handle, pynvml.NVML_CLOCK_VIDEO)
|
||||
|
||||
return info
|
||||
|
||||
@register_task('device:temperature')
|
||||
def _get_temp_info(self, handle):
|
||||
info = 0
|
||||
try:
|
||||
info = pynvml.nvmlDeviceGetTemperature(handle, pynvml.NVML_TEMPERATURE_GPU)
|
||||
except pynvml.NVMLError or AttributeError:
|
||||
info = pynvml.nvmlDeviceGetTemperatureV1(handle, pynvml.NVML_TEMPERATURE_GPU)
|
||||
return info
|
||||
|
||||
@register_task('device:utilization')
|
||||
def _get_uti_info(self, handle):
|
||||
info = defaultdict()
|
||||
info['gpu'] = pynvml.nvmlDeviceGetUtilizationRates(handle).gpu
|
||||
info['memory'] = pynvml.nvmlDeviceGetUtilizationRates(handle).memory
|
||||
|
||||
return info
|
||||
|
||||
@register_task('device:processes')
|
||||
def _get_proc_uti(self, handle):
|
||||
info = list()
|
||||
for p in pynvml.nvmlDeviceGetComputeRunningProcesses(handle):
|
||||
p.__dict__['name'] = pynvml.nvmlSystemGetProcessName(p.pid)
|
||||
p.__dict__['type'] = 'Compute'
|
||||
info.append(p.__dict__)
|
||||
|
||||
for p in pynvml.nvmlDeviceGetGraphicsRunningProcesses(handle):
|
||||
p.__dict__['name'] = pynvml.nvmlSystemGetProcessName(p.pid)
|
||||
p.__dict__['type'] = 'Graphics'
|
||||
info.append(p.__dict__)
|
||||
|
||||
for p in pynvml.nvmlDeviceGetMPSComputeRunningProcesses(handle):
|
||||
p.__dict__['name'] = pynvml.nvmlSystemGetProcessName(p.pid)
|
||||
p.__dict__['type'] = 'MPS'
|
||||
info.append(p.__dict__)
|
||||
|
||||
return info
|
||||
|
||||
@register_task('device:fan')
|
||||
def _get_fan_info(self, handle):
|
||||
info = defaultdict()
|
||||
try:
|
||||
info['speed'] = pynvml.nvmlDeviceGetFanSpeedRPM(handle).speed
|
||||
except AttributeError:
|
||||
info['speed'] = pynvml.nvmlDeviceGetFanSpeed(handle)
|
||||
except pynvml.NVMLError:
|
||||
info['speed'] = pynvml.nvmlDeviceGetFanSpeed_v2(handle, 0)
|
||||
except:
|
||||
info['speed'] = 0
|
||||
return info
|
||||
|
||||
@register_task('device:name')
|
||||
def _get_device_name(self, handle):
|
||||
return pynvml.nvmlDeviceGetName(handle)
|
||||
|
||||
@register_task('device:power')
|
||||
def _get_device_power(self, handle):
|
||||
info = defaultdict()
|
||||
info['current'] = pynvml.nvmlDeviceGetPowerUsage(handle)
|
||||
info['max'] = pynvml.nvmlDeviceGetPowerManagementLimit(handle)
|
||||
return info
|
||||
|
||||
@register_task('system:version')
|
||||
def _get_device_version(self):
|
||||
info = defaultdict()
|
||||
info['driver'] = pynvml.nvmlSystemGetDriverVersion()
|
||||
|
||||
try:
|
||||
info['cuda'] = pynvml.nvmlSystemGetCudaDriverVersion()
|
||||
except pynvml.NVMLError or AttributeError:
|
||||
info['cuda'] = pynvml.nvmlSystemGetCudaDriverVersion_v2()
|
||||
|
||||
return info
|
||||
|
||||
@register_task('system:count')
|
||||
def _get_device_count(self):
|
||||
info = 0
|
||||
info = pynvml.nvmlDeviceGetCount()
|
||||
return info
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
nvidia = NVIDIA()
|
||||
print(nvidia.get_all_device_info())
|
||||
158
mod/project/docker/app/gpu/tools.py
Normal file
158
mod/project/docker/app/gpu/tools.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import Tuple
|
||||
|
||||
from mod.project.docker.app.gpu.constants import CMD
|
||||
from mod.project.docker.app.gpu.nvidia import NVIDIA
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.append('/www/server/panel/class')
|
||||
|
||||
import public
|
||||
|
||||
|
||||
class GPUTool:
|
||||
gpu_option = None
|
||||
option_default = None
|
||||
|
||||
@staticmethod
|
||||
def __get_linux_distribution():
|
||||
"""检测系统是否为 Debian/Ubuntu 或 CentOS/Red Hat 系列"""
|
||||
try:
|
||||
# 优先解析 /etc/os-release
|
||||
with open("/etc/os-release", "r", encoding="utf-8") as f:
|
||||
os_release = {}
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and "=" in line:
|
||||
key, value = line.split("=", 1)
|
||||
os_release[key] = value.strip('"')
|
||||
|
||||
dist_id = os_release.get("ID", "").lower()
|
||||
id_like = os_release.get("ID_LIKE", "").lower()
|
||||
|
||||
# 根据 ID 或 ID_LIKE 判断
|
||||
if dist_id in ["debian", "ubuntu"]:
|
||||
return "debian"
|
||||
elif dist_id in ["centos", "rhel", "fedora"]:
|
||||
return "centos"
|
||||
elif "debian" in id_like:
|
||||
return "debian"
|
||||
elif "rhel" in id_like or "fedora" in id_like:
|
||||
return "centos"
|
||||
|
||||
except FileNotFoundError:
|
||||
# 如果 /etc/os-release 不存在,检查其他文件
|
||||
if os.path.exists("/etc/debian_version"):
|
||||
return "debian"
|
||||
elif os.path.exists("/etc/redhat-release"):
|
||||
return "centos"
|
||||
|
||||
except Exception:
|
||||
raise ValueError("System Distribution Is Unknown")
|
||||
|
||||
@classmethod
|
||||
def __gpu_default_setting(cls) -> Tuple[bool, bool]:
|
||||
"""
|
||||
检测是否开启GPU
|
||||
Returns:
|
||||
gpu_option: 返回是否开启GPU选择
|
||||
option_default: 默认GPU选择是否开启
|
||||
"""
|
||||
if cls.gpu_option is not None and cls.option_default is not None:
|
||||
return cls.gpu_option, cls.option_default
|
||||
|
||||
driver = NVIDIA()
|
||||
# 如果不支持直接返回
|
||||
if driver.support is None or driver.support is False:
|
||||
cls.gpu_option = False
|
||||
cls.option_default = False
|
||||
return cls.gpu_option, cls.option_default
|
||||
|
||||
# 如果支持则检查显存大小
|
||||
device_info = driver.get_all_device_info()
|
||||
mem_size = 0
|
||||
for _, _device in device_info.items():
|
||||
mem_size = mem_size + _device.get('memory', {}).get('size', 0)
|
||||
if mem_size > 3:
|
||||
cls.gpu_option = True
|
||||
cls.option_default = True
|
||||
else:
|
||||
cls.gpu_option = True
|
||||
cls.option_default = False
|
||||
|
||||
return cls.gpu_option, cls.option_default
|
||||
|
||||
@classmethod
|
||||
def register_app_gpu_option(cls, app):
|
||||
option, default = cls.__gpu_default_setting()
|
||||
for field in app.get('field', []):
|
||||
if option == False and field.get('attr', '') == 'gpu':
|
||||
app['field'].remove(field)
|
||||
elif option == True and field.get('attr', '') == 'gpu':
|
||||
field['default'] = default
|
||||
field['suffix'] = field['suffix'] + ' | 已默认设置为{}'.format(default)
|
||||
# public.print_log("\n\n\n\n{}\n\n\n\n".format(field['suffix']))
|
||||
return app
|
||||
|
||||
@staticmethod
|
||||
def is_install_ctk():
|
||||
stdout, stderr = public.ExecShell(CMD.CTK.CheckVersion)
|
||||
if len(stderr) != 0:
|
||||
return False
|
||||
if not stdout.lower().find('version'):
|
||||
public.print_log("Not Nvidia Container Toolkit")
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def __ctk_install_cmd_apt(cls, app_log):
|
||||
return ("{get_gpg_key} >> {app_log};"
|
||||
"{add_sources_list} >> {app_log};"
|
||||
"{apt_update} >> {app_log};"
|
||||
"{install} >> {app_log}"
|
||||
.format(get_gpg_key=CMD.CTK.APT.GetGPGKey,
|
||||
add_sources_list=CMD.CTK.APT.AddSourcesList,
|
||||
apt_update=CMD.CTK.APT.APTUpdate,
|
||||
install=CMD.CTK.APT.Install,
|
||||
app_log=app_log
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def __ctk_install_cmd_yum(cls, app_log):
|
||||
return ("{add_repo} >> {app_log};"
|
||||
"{install} >> {app_log}"
|
||||
.format(add_repo=CMD.CTK.YUM.AddRepo,
|
||||
install=CMD.CTK.YUM.Install,
|
||||
app_log=app_log
|
||||
))
|
||||
|
||||
@classmethod
|
||||
def __config_docker(cls, app_log):
|
||||
return ("{runtime} >> {app_log};"
|
||||
"{restart} >> {app_log}"
|
||||
.format(runtime=CMD.CTK.ConfigureDocker.Runtime,
|
||||
restart=CMD.CTK.ConfigureDocker.Restart,
|
||||
app_log=app_log))
|
||||
|
||||
@classmethod
|
||||
def ctk_install_cmd(cls, app_log):
|
||||
dtb = cls.__get_linux_distribution()
|
||||
cmd = ''
|
||||
if dtb == 'debian':
|
||||
cmd = (
|
||||
"{install_cmd};"
|
||||
"{config_docker}"
|
||||
.format(
|
||||
install_cmd=cls.__ctk_install_cmd_apt(app_log),
|
||||
config_docker=cls.__config_docker(app_log),
|
||||
))
|
||||
elif dtb == 'centos':
|
||||
cmd = (
|
||||
"{install_cmd};"
|
||||
"{config_docker}"
|
||||
.format(
|
||||
install_cmd=cls.__ctk_install_cmd_yum(app_log),
|
||||
config_docker=cls.__config_docker(app_log),
|
||||
))
|
||||
return cmd
|
||||
0
mod/project/docker/app/gpu/type.py
Normal file
0
mod/project/docker/app/gpu/type.py
Normal file
0
mod/project/docker/app/sub_app/__init__.py
Normal file
0
mod/project/docker/app/sub_app/__init__.py
Normal file
16
mod/project/docker/app/sub_app/base.py
Normal file
16
mod/project/docker/app/sub_app/base.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# +-------------------------------------------------------------------
|
||||
# | YakPanel
|
||||
# +-------------------------------------------------------------------
|
||||
# | Copyleft (c) 2015-2099 YakPanel(www.yakpanel.com) All lefts reserved.
|
||||
# +-------------------------------------------------------------------
|
||||
# | Author: wzz
|
||||
# | email : wzz@yakpanel.com
|
||||
# +-------------------------------------------------------------------
|
||||
# +-------------------------------------------------------------------
|
||||
# | docker sub_app 管理模型 -
|
||||
# +-------------------------------------------------------------------
|
||||
|
||||
class base():
|
||||
def __init__(self):
|
||||
pass
|
||||
198
mod/project/docker/app/sub_app/downModel.py
Normal file
198
mod/project/docker/app/sub_app/downModel.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# +-------------------------------------------------------------------
|
||||
# | YakPanel
|
||||
# +-------------------------------------------------------------------
|
||||
# | Copyleft (c) 2015-2099 YakPanel(www.yakpanel.com) All lefts reserved.
|
||||
# +-------------------------------------------------------------------
|
||||
# | Author: wzz
|
||||
# | email : wzz@yakpanel.com
|
||||
# +-------------------------------------------------------------------
|
||||
# +-------------------------------------------------------------------
|
||||
# | docker sub_app 管理模型 -
|
||||
# +-------------------------------------------------------------------
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from collections import deque
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.append('/www/server/panel/class')
|
||||
|
||||
import public
|
||||
|
||||
def download_model(service_name, model_name, model_version, ollama_url, app_cmd_log):
|
||||
"""
|
||||
下载Ollama模型的具体实现
|
||||
@param service_name: 服务名称
|
||||
@param model_name: 模型名称
|
||||
@param model_version: 模型版本
|
||||
@param ollama_url: Ollama API URL
|
||||
@param app_cmd_log: 日志文件路径
|
||||
"""
|
||||
def start_download():
|
||||
url = ollama_url + "/api/pull"
|
||||
|
||||
# 准备请求数据
|
||||
data = {
|
||||
"model": "{}:{}".format(model_name, model_version),
|
||||
"stream": True
|
||||
}
|
||||
|
||||
try:
|
||||
import requests
|
||||
response = requests.post(url, json=data, stream=True)
|
||||
|
||||
with open(app_cmd_log, 'a') as log_file:
|
||||
log_file.write('{} model is being downloaded, and may need to wait more than 1-30 minutes...\n'.format(model_name))
|
||||
|
||||
download_tag = None
|
||||
last_completed = 0
|
||||
last_time = time.time()
|
||||
# 使用双端队列存储最近10秒的速度
|
||||
speed_history = deque(maxlen=60)
|
||||
|
||||
count_sum = 0
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
json_response = json.loads(line)
|
||||
status = json_response.get("status", "")
|
||||
|
||||
# 记录下载进度
|
||||
if "pulling" in status:
|
||||
status = status.split(" ")
|
||||
if download_tag is None or status[1] != download_tag:
|
||||
download_tag = status[1]
|
||||
last_completed = 0
|
||||
last_time = time.time()
|
||||
speed_history.clear()
|
||||
|
||||
completed = json_response.get("completed", 0)
|
||||
total = json_response.get("total", 0)
|
||||
|
||||
if total > 0:
|
||||
# 计算下载速度
|
||||
current_time = time.time()
|
||||
time_diff = current_time - last_time
|
||||
if time_diff >= 1: # 每秒更新一次
|
||||
bytes_diff = completed - last_completed
|
||||
speed = bytes_diff / time_diff # bytes per second
|
||||
|
||||
# 存储当前速度
|
||||
count_sum += 1
|
||||
if count_sum > 5:
|
||||
speed_history.append(speed)
|
||||
|
||||
# 检查速度是否异常
|
||||
avg_speed = None
|
||||
if len(speed_history) >= 10:
|
||||
avg_speed = sum(list(speed_history)[:-1]) / (len(speed_history) - 1)
|
||||
current_speed = speed_history[-1]
|
||||
|
||||
if current_speed < 1024000 and avg_speed < 1536000: # 当前速度小于1.2MB/s且平均速度小于1.5MB/s
|
||||
log_file.write('Detected that the download speed is too low and is trying to reset the download...\n')
|
||||
log_file.flush()
|
||||
return False # 返回False表示需要重新下载
|
||||
|
||||
if current_speed < (avg_speed / 4) and avg_speed > 1024: # 确保有足够的平均速度
|
||||
log_file.write('Abnormal download speed or CF slowdown detected, trying to reset the download...\n')
|
||||
log_file.flush()
|
||||
return False # 返回False表示需要重新下载
|
||||
|
||||
# 转换速度单位
|
||||
speed_str = ""
|
||||
if speed < 1024:
|
||||
speed_str = "{:.2f} B/s".format(speed)
|
||||
elif speed < 1024 * 1024:
|
||||
speed_str = "{:.2f} KB/s".format(speed / 1024)
|
||||
else:
|
||||
speed_str = "{:.2f} MB/s".format(speed / (1024 * 1024))
|
||||
|
||||
avg_speed_str = ""
|
||||
if not avg_speed is None:
|
||||
if avg_speed < 1024:
|
||||
avg_speed_str = "{:.2f} B/s".format(avg_speed)
|
||||
elif avg_speed < 1024 * 1024:
|
||||
avg_speed_str = "{:.2f} KB/s".format(avg_speed / 1024)
|
||||
else:
|
||||
avg_speed_str = "{:.2f} MB/s".format(avg_speed / (1024 * 1024))
|
||||
|
||||
progress = (completed / total) * 100
|
||||
log_file.write('File: {}, Download Progress: {:.2f}%, Average Speed: {}, Current Speed: {}\n'.format(
|
||||
download_tag,
|
||||
progress,
|
||||
avg_speed_str,
|
||||
speed_str
|
||||
))
|
||||
log_file.flush()
|
||||
|
||||
# 更新上次的数据
|
||||
last_completed = completed
|
||||
last_time = current_time
|
||||
else:
|
||||
log_file.write(status + '\n')
|
||||
log_file.flush()
|
||||
|
||||
# 下载完成后验证模型是否存在
|
||||
verify_cmd = "docker-compose -p {service_name} exec -it {service_name_} ollama list | grep {model_name}:{model_version}".format(
|
||||
service_name=service_name.lower(),
|
||||
service_name_=service_name,
|
||||
model_name=model_name,
|
||||
model_version=model_version
|
||||
)
|
||||
result = public.ExecShell(verify_cmd)[0]
|
||||
|
||||
if model_name in result:
|
||||
log_file.write('bt_successful\n')
|
||||
return True
|
||||
else:
|
||||
public.writeFile("/tmp/{model_name}:{model_version}.failed".format(
|
||||
model_name=model_name,
|
||||
model_version=model_version,
|
||||
), "failed")
|
||||
log_file.write('bt_failed\n')
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
# 发生异常时记录错误并标记失败
|
||||
with open(app_cmd_log, 'a') as log_file:
|
||||
log_file.write('Download failed: {}\n'.format(str(e)))
|
||||
log_file.write('bt_failed\n')
|
||||
public.writeFile("/tmp/{model_name}:{model_version}.failed".format(
|
||||
model_name=model_name,
|
||||
model_version=model_version,
|
||||
), "failed")
|
||||
return False
|
||||
|
||||
# 设置下载状态标记
|
||||
public.ExecShell("echo 'downloading' > /tmp/{model_name}:{model_version}.pl".format(
|
||||
model_name=model_name,
|
||||
model_version=model_version
|
||||
))
|
||||
public.ExecShell("echo 'downloading' > /tmp/nocandown.pl")
|
||||
public.ExecShell("rm -f /tmp/{model_name}:{model_version}.failed".format(
|
||||
model_name=model_name,
|
||||
model_version=model_version,
|
||||
))
|
||||
|
||||
try:
|
||||
max_retries = 30
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
if retry_count > 0:
|
||||
with open(app_cmd_log, 'a') as log_file:
|
||||
log_file.write('\n{} retry in progress...\n'.format(retry_count + 1))
|
||||
|
||||
if start_download():
|
||||
break
|
||||
|
||||
retry_count += 1
|
||||
time.sleep(3) # 重试前等待3秒
|
||||
|
||||
finally:
|
||||
# 清理状态文件
|
||||
public.ExecShell("rm -f /tmp/{model_name}:{model_version}.pl".format(
|
||||
model_name=model_name,
|
||||
model_version=model_version,
|
||||
))
|
||||
public.ExecShell("rm -f /tmp/nocandown.pl")
|
||||
324
mod/project/docker/app/sub_app/ollamaMod.py
Normal file
324
mod/project/docker/app/sub_app/ollamaMod.py
Normal file
@@ -0,0 +1,324 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# +-------------------------------------------------------------------
|
||||
# | YakPanel
|
||||
# +-------------------------------------------------------------------
|
||||
# | Copyleft (c) 2015-2099 YakPanel(www.yakpanel.com) All lefts reserved.
|
||||
# +-------------------------------------------------------------------
|
||||
# | Author: wzz
|
||||
# | email : wzz@yakpanel.com
|
||||
# +-------------------------------------------------------------------
|
||||
# +-------------------------------------------------------------------
|
||||
# | docker sub_app 管理模型 -
|
||||
# +-------------------------------------------------------------------
|
||||
import json
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.append('/www/server/panel/class')
|
||||
|
||||
import public
|
||||
from mod.project.docker.app.base import App
|
||||
|
||||
class OllamaBase(App):
|
||||
def __init__(self):
|
||||
super(OllamaBase, self).__init__()
|
||||
self.ollama_port = "11434"
|
||||
self.ollama_local_url = "http://127.0.0.1:{}".format(self.ollama_port)
|
||||
|
||||
def set_ollama_port(self, port):
|
||||
self.ollama_port = port
|
||||
self.ollama_local_url = self.ollama_local_url.format(port)
|
||||
return self
|
||||
|
||||
def set_ollama_local_url(self, port):
|
||||
self.ollama_local_url = "http://127.0.0.1:{}".format(port)
|
||||
return self
|
||||
|
||||
|
||||
class OllamaMod(OllamaBase):
|
||||
|
||||
def __init__(self):
|
||||
super(OllamaMod, self).__init__()
|
||||
|
||||
# 2025/2/8 11:47 获取本地所有的models
|
||||
# https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models
|
||||
def list_local_models(self):
|
||||
uri = "/api/tags"
|
||||
|
||||
ps_json, stderr = public.ExecShell("docker-compose -p {service_name} ps --format json | {grep_v}".format(
|
||||
service_name=self.service_name.lower(),
|
||||
grep_v=self.grep_version,
|
||||
))
|
||||
if "Segmentation fault" in ps_json:
|
||||
return []
|
||||
|
||||
if not ps_json.startswith("["):
|
||||
ps = json.loads("[" + ps_json.strip().replace("\n", ",") + "]")
|
||||
else:
|
||||
ps = json.loads(ps_json.strip().replace("\n", ","))
|
||||
|
||||
try:
|
||||
p_port = "11434"
|
||||
for i in ps:
|
||||
if "ollama/ollama" in i["Image"]:
|
||||
if len(i["Publishers"]) == 0: break
|
||||
p_port = i["Publishers"][0]["PublishedPort"]
|
||||
except:
|
||||
p_port = "11434"
|
||||
|
||||
self.set_ollama_local_url(p_port)
|
||||
url = self.ollama_local_url + uri
|
||||
response = public.HttpGet(url)
|
||||
if not response: return []
|
||||
response = json.loads(response)
|
||||
|
||||
if "models" in response:
|
||||
models = response["models"]
|
||||
for i in models:
|
||||
i["version"] = i["name"].split(":")[-1] if ":" in i["name"] else i["name"]
|
||||
i["l_name"] = i["name"].split(":")[0] if ":" in i["name"] else i["name"]
|
||||
return models
|
||||
return []
|
||||
|
||||
# 2025/2/10 15:52 获取指定模型的信息
|
||||
# https://github.com/ollama/ollama/blob/main/docs/api.md#show-model-information
|
||||
def show_model_info(self, get):
|
||||
'''
|
||||
@name 获取指定模型的信息
|
||||
'''
|
||||
get.model_name = get.get("model_name", None)
|
||||
if get.model_name is None:
|
||||
return public.return_message(-1, 0, public.lang("model_name parameter cannot be null"))
|
||||
get.model_version = get.get("model_version", None)
|
||||
if get.model_version is None:
|
||||
return public.return_message(-1, 0, public.lang("model_version parameter cannot be null"))
|
||||
get.service_name = get.get("service_name", None)
|
||||
if get.service_name is None:
|
||||
return public.return_message(-1, 0, public.lang("service_name parameter cannot be null"))
|
||||
|
||||
self.set_service_name(get.service_name)
|
||||
uri = "/api/show"
|
||||
ps_json, stderr = public.ExecShell("docker-compose -p {service_name} ps --format json | {grep_v}".format(
|
||||
service_name=self.service_name.lower(),
|
||||
grep_v=self.grep_version,
|
||||
))
|
||||
if "Segmentation fault" in ps_json:
|
||||
return []
|
||||
|
||||
if not ps_json.startswith("["):
|
||||
ps = json.loads("[" + ps_json.strip().replace("\n", ",") + "]")
|
||||
else:
|
||||
ps = json.loads(ps_json.strip().replace("\n", ","))
|
||||
|
||||
try:
|
||||
p_port = "11434"
|
||||
for i in ps:
|
||||
if "ollama/ollama" in i["Image"]:
|
||||
if len(i["Publishers"]) == 0: break
|
||||
p_port = i["Publishers"][0]["PublishedPort"]
|
||||
except:
|
||||
p_port = "11434"
|
||||
|
||||
self.set_ollama_local_url(p_port)
|
||||
|
||||
url = self.ollama_local_url + uri
|
||||
param = {"model": "{}:{}".format(get.model_name, get.model_version)}
|
||||
|
||||
import requests
|
||||
response = requests.post(url, data=json.dumps(param), timeout=10)
|
||||
|
||||
return public.return_message(0, 0, response.json())
|
||||
|
||||
# 2025/2/10 14:51 获取在线的所有models
|
||||
def list_online_models(self):
|
||||
'''
|
||||
@name 获取在线的所有models
|
||||
'''
|
||||
if not os.path.exists(self.ollama_online_models_file):
|
||||
public.downloadFile(public.get_url() + '/src/dk_app/yakpanel/apps/ollama_model.json', self.ollama_online_models_file)
|
||||
|
||||
try:
|
||||
models = json.loads(public.readFile(self.ollama_online_models_file))
|
||||
|
||||
res = []
|
||||
for i in models:
|
||||
res.append({
|
||||
"name": i["name"],
|
||||
"description": i["zh_cn_msg"],
|
||||
"version": i["parameters"],
|
||||
"size": i["size"],
|
||||
"can_down": True,
|
||||
})
|
||||
|
||||
return res
|
||||
except:
|
||||
return []
|
||||
|
||||
# 2025/2/10 14:54 获取模型列表
|
||||
def get_models_list(self, get):
|
||||
'''
|
||||
@name 获取模型列表
|
||||
'''
|
||||
get.search = get.get("search", "")
|
||||
get.p = get.get("p/d", 1)
|
||||
get.row = get.get("limit/d", 20)
|
||||
get.service_name = get.get("service_name", None)
|
||||
if get.service_name is None:
|
||||
return public.return_message(-1, 0, public.lang("service_name parameter cannot be null"))
|
||||
get.status = get.get("status", "all")
|
||||
self.set_service_name(get.service_name)
|
||||
|
||||
local_models = self.list_local_models()
|
||||
public.print_log(local_models)
|
||||
online_models = self.list_online_models()
|
||||
res = []
|
||||
can_down = True
|
||||
if os.path.exists("/tmp/nocandown.pl"):
|
||||
can_down = False
|
||||
|
||||
# 2025/2/10 14:55 合并两个列表,增加status字段,已经安装了值为installed
|
||||
for i in online_models:
|
||||
i["can_down"] = can_down
|
||||
|
||||
i["status"] = "uninstall"
|
||||
for j in local_models:
|
||||
if i["name"] == j["l_name"]:
|
||||
i["status"] = "installed" if i["version"] == j["version"] else "uninstall"
|
||||
|
||||
if os.path.exists("/tmp/{model_name}:{model_version}.failed".format(
|
||||
model_name=i["name"],
|
||||
model_version=i["version"],
|
||||
)):
|
||||
i["status"] = "failed"
|
||||
|
||||
if os.path.exists("/tmp/{model_name}:{model_version}.pl".format(
|
||||
model_name=i["name"],
|
||||
model_version=i["version"],
|
||||
)):
|
||||
i["status"] = "downloading"
|
||||
|
||||
if i["status"] in ("installed", "failed", "downloading"):
|
||||
break
|
||||
|
||||
if get.status != "all":
|
||||
if get.status != i["status"]: continue
|
||||
if get.search != "":
|
||||
if get.search not in i["name"] and get.search not in i["description"]: continue
|
||||
|
||||
res.append(i)
|
||||
|
||||
page_data = self.get_page(res, get)
|
||||
return self.pageResult(True, data=page_data["data"], page=page_data["page"])
|
||||
|
||||
# 2025/2/17 16:34 给指定应用安装指定模型
|
||||
def down_models(self, get):
|
||||
'''
|
||||
@name 给指定应用安装指定模型
|
||||
@param service_name 服务名称
|
||||
@param model_name 模型名称
|
||||
@param model_version 模型版本
|
||||
'''
|
||||
get.service_name = get.get("service_name", None)
|
||||
if get.service_name is None:
|
||||
return public.return_message(-1, 0, public.lang("service_name parameter cannot be null"))
|
||||
get.model_name = get.get("model_name", None)
|
||||
if get.model_name is None:
|
||||
return public.return_message(-1, 0, public.lang("model_name parameter cannot be null"))
|
||||
get.model_version = get.get("model_version", None)
|
||||
if get.model_version is None:
|
||||
return public.return_message(-1, 0, public.lang("model_version parameter cannot be null"))
|
||||
|
||||
self.set_service_name(get.service_name)
|
||||
|
||||
# 获取容器信息
|
||||
ps_json, stderr = public.ExecShell("docker-compose -p {service_name} ps --format json | {grep_v}".format(
|
||||
service_name=self.service_name.lower(),
|
||||
grep_v=self.grep_version,
|
||||
))
|
||||
if "Segmentation fault" in ps_json:
|
||||
return public.return_message(-1, 0, public.lang("Failed to get container information, docker-compose execution is exceptional!"))
|
||||
|
||||
if not ps_json.startswith("["):
|
||||
ps = json.loads("[" + ps_json.strip().replace("\n", ",") + "]")
|
||||
else:
|
||||
ps = json.loads(ps_json.strip().replace("\n", ","))
|
||||
|
||||
try:
|
||||
p_port = "11434"
|
||||
for i in ps:
|
||||
if "ollama/ollama" in i["Image"]:
|
||||
if len(i["Publishers"]) == 0: break
|
||||
p_port = i["Publishers"][0]["PublishedPort"]
|
||||
except:
|
||||
p_port = "11434"
|
||||
|
||||
self.set_ollama_local_url(p_port)
|
||||
|
||||
# 设置日志文件
|
||||
self.set_cmd_log()
|
||||
public.ExecShell("echo > {}".format(self.app_cmd_log))
|
||||
|
||||
# 导入下载模块并执行下载
|
||||
from mod.project.docker.app.sub_app.downModel import download_model
|
||||
import threading
|
||||
|
||||
# 创建新线程执行下载
|
||||
download_thread = threading.Thread(
|
||||
target=download_model,
|
||||
args=(
|
||||
get.service_name,
|
||||
get.model_name,
|
||||
get.model_version,
|
||||
self.ollama_local_url,
|
||||
self.app_cmd_log
|
||||
)
|
||||
)
|
||||
download_thread.daemon = True
|
||||
download_thread.start()
|
||||
|
||||
return public.return_message(0, 0, public.lang("The model is being downloaded, please check the logs later"))
|
||||
|
||||
# 2025/2/10 15:50 删除指定应用的指定模型
|
||||
def del_models(self, get):
|
||||
'''
|
||||
@name 删除指定应用的指定模型
|
||||
'''
|
||||
get.service_name = get.get("service_name", None)
|
||||
if get.service_name is None:
|
||||
return public.return_message(-1, 0, public.lang("service_name parameter cannot be null"))
|
||||
get.model_name = get.get("model_name", None)
|
||||
if get.model_name is None:
|
||||
return public.return_message(-1, 0, public.lang("model_name parameter cannot be null"))
|
||||
get.model_version = get.get("model_version", None)
|
||||
if get.model_version is None:
|
||||
return public.return_message(-1, 0, public.lang("model_version parameter cannot be null"))
|
||||
|
||||
self.set_service_name(get.service_name)
|
||||
|
||||
ps_json, stderr = public.ExecShell("docker-compose -p {service_name} ps --format json | {grep_v}".format(
|
||||
service_name=self.service_name.lower(),
|
||||
grep_v=self.grep_version,
|
||||
))
|
||||
if "Segmentation fault" in ps_json:
|
||||
return public.return_message(0, 0, public.lang("Failed to delete model, docker-compose execution exception!"))
|
||||
|
||||
if not ps_json.startswith("["):
|
||||
ps = json.loads("[" + ps_json.strip().replace("\n", ",") + "]")
|
||||
else:
|
||||
ps = json.loads(ps_json.strip().replace("\n", ","))
|
||||
|
||||
serviceName = get.service_name
|
||||
if len(ps) == 2:
|
||||
serviceName = "ollama"
|
||||
|
||||
cmd = ("docker-compose -p {service_name} exec -it {serviceName} ollama rm {model_name}:{model_version}".format(
|
||||
service_name=get.service_name.lower(),
|
||||
serviceName=serviceName,
|
||||
model_name=get.model_name,
|
||||
model_version=get.model_version,
|
||||
))
|
||||
public.ExecShell(cmd)
|
||||
return public.return_message(0, 0, public.lang("Successful deletion of model!"))
|
||||
0
mod/project/docker/apphub/__init__.py
Normal file
0
mod/project/docker/apphub/__init__.py
Normal file
258
mod/project/docker/apphub/apphubManage.py
Normal file
258
mod/project/docker/apphub/apphubManage.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: csj <csj@yakpanel.com>
|
||||
# -------------------------------------------------------------------
|
||||
# ------------------------------
|
||||
# docker应用商店 apphub 业务类
|
||||
# ------------------------------
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
import public
|
||||
from mod.base.git_tool import GitTool
|
||||
from mod.project.docker.app.base import App
|
||||
import mod.base.git_tool.install as GitInstall
|
||||
|
||||
class AppHub():
|
||||
_instance = None
|
||||
hub_config_path = os.path.join(App.dk_project_path,'dk_app','apphub_config.json') #/www/dk_project/dk_app/apphub_config.json
|
||||
hub_home_path = os.path.join(App.dk_project_path,'dk_app','apphub','apphub') #/www/dk_project/dk_app/apphub/apphub
|
||||
hub_apps_path = os.path.join(hub_home_path,'apps.json') #/www/dk_project/dk_app/apphub/apphub/apps.json
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if not hasattr(cls, "_instance") or cls._instance is None:
|
||||
cls._instance = super(AppHub, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_config(cls):
|
||||
'''
|
||||
@name 获取外部应用配置
|
||||
'''
|
||||
if not os.path.exists(cls.hub_config_path):
|
||||
apphub_config = {
|
||||
"git_config": {
|
||||
"git_url": "",
|
||||
"git_branch": "main",
|
||||
"user_config": {
|
||||
"name": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
public.writeFile(cls.hub_config_path, json.dumps(apphub_config))
|
||||
return json.loads(public.readFile(cls.hub_config_path))
|
||||
|
||||
def install_apphub(self,get):
|
||||
|
||||
git_install = GitInstall.install_git()
|
||||
if not git_install:
|
||||
return public.return_message(-1, 0, public.lang("If the installation fails, check the network or install git manually"))
|
||||
|
||||
return public.return_message(0, 0, public.lang("The environment was successfully installed"))
|
||||
|
||||
def get_hub_apps(self):
|
||||
'''
|
||||
@name 获取外部应用列表
|
||||
'''
|
||||
res = []
|
||||
try:
|
||||
if os.path.exists(AppHub.hub_apps_path):
|
||||
res=json.loads(public.readFile(self.hub_apps_path))
|
||||
except:
|
||||
pass
|
||||
return res
|
||||
|
||||
def generate_apphub(self, get):
|
||||
'''
|
||||
@name 解析外部应用列表
|
||||
'''
|
||||
apps = []
|
||||
if not os.path.isdir(self.hub_home_path):
|
||||
return public.return_message(-1, 0, public.lang("The apphub directory does not exist"))
|
||||
for name in os.listdir(self.hub_home_path):
|
||||
app_dir = os.path.join(self.hub_home_path, name)
|
||||
if not os.path.isdir(app_dir): continue
|
||||
|
||||
app_info=public.readFile(os.path.join(app_dir, 'app.json'))
|
||||
if not app_info: continue
|
||||
|
||||
try:
|
||||
app_info = json.loads(app_info)
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
if "reuse" not in app_info: app_info["reuse"] = True
|
||||
if "icon" not in app_info: app_info["icon"] = ""
|
||||
if "sort" not in app_info: app_info["sort"] = 999
|
||||
if "cpu" not in app_info: app_info["cpu"] = 0
|
||||
if "mem" not in app_info: app_info["mem"] = 0
|
||||
if "disk" not in app_info: app_info["disk"] = 10240
|
||||
if "installed" not in app_info: app_info["installed"] = False
|
||||
if "updateat" not in app_info: app_info["updateat"] = 0
|
||||
|
||||
apps.append(app_info)
|
||||
|
||||
self.apphub_apps = apps
|
||||
|
||||
public.writeFile(self.hub_apps_path, json.dumps(apps, indent=4, ensure_ascii=False))
|
||||
|
||||
self.generate_apphub_icon()
|
||||
|
||||
return public.return_message(0, 0, public.lang("The resolution was successful"))
|
||||
|
||||
def generate_apphub_icon(self):
|
||||
'''
|
||||
@name 创建外部应用图标
|
||||
#/static/img/soft_ico/apphub/ico-apphub_xxx.png
|
||||
'''
|
||||
apphub_ico_path = "{}/YakPanel/static/img/soft_ico/apphub/".format(public.get_panel_path())
|
||||
if os.path.exists(apphub_ico_path):
|
||||
public.ExecShell("rm -rf {}".format(apphub_ico_path))
|
||||
public.ExecShell("mkdir -p {}".format(apphub_ico_path))
|
||||
|
||||
for name in os.listdir(self.hub_home_path):
|
||||
app_dir = os.path.join(self.hub_home_path, name,'icon.png')
|
||||
if not os.path.exists(app_dir): continue
|
||||
|
||||
app_icon_path = os.path.join(apphub_ico_path, "ico-apphub_{}.png".format(name))
|
||||
public.ExecShell("cp {} {}".format(app_dir,app_icon_path))
|
||||
return True
|
||||
|
||||
def set_apphub_git(self, get):
|
||||
'''
|
||||
@name 设置git配置
|
||||
@param get: git_url, git_branch, user, password
|
||||
'''
|
||||
config = self.get_config()
|
||||
git_config = config.get("git_config", {})
|
||||
git_config["git_url"] = get.git_url.strip()
|
||||
git_config["git_branch"] = get.git_branch.strip()
|
||||
if "name" in get and "password" in get:
|
||||
git_config["user_config"] = {
|
||||
"name": get.get("name", ""),
|
||||
"password": get.get("password", "")
|
||||
}
|
||||
config["git_config"] = git_config
|
||||
public.writeFile(self.hub_config_path, json.dumps(config, indent=4, ensure_ascii=False))
|
||||
return public.return_message(0, 0, public.lang("GIT INFORMATION IS SUCCESSFULLY CONFIGURED"))
|
||||
|
||||
def import_git_apphub(self,get):
|
||||
'''
|
||||
@name 从git导入外部应用
|
||||
@param None
|
||||
'''
|
||||
if not GitInstall.installed():
|
||||
return public.return_message(-1, 0, public.lang("If you don't have a git environment, please install git first"))
|
||||
|
||||
abs_path = os.path.dirname(self.hub_home_path)
|
||||
if not os.path.exists(abs_path): os.makedirs(abs_path)
|
||||
|
||||
gitconfig = self.get_config()
|
||||
if not gitconfig or not gitconfig.get("git_config", {}).get("git_url", ""):
|
||||
return public.return_message(-1, 0, public.lang("Please set the git information first"))
|
||||
|
||||
git_url = gitconfig.get("git_config", {}).get("git_url", "")
|
||||
git_user = gitconfig.get("git_config", {}).get("user_config", {})
|
||||
git_branch = gitconfig.get("git_config", {}).get("git_branch", {})
|
||||
|
||||
git = GitTool(project_path=abs_path, git_url=git_url,user_config=git_user,git_id="-1")
|
||||
public.ExecShell("rm -rf /tmp/git_-1_log.log")
|
||||
res = git.pull(git_branch)
|
||||
|
||||
if res is not None:
|
||||
return public.return_message(-1, 0, public.lang("Import from git failed"))
|
||||
|
||||
#解析全部应用
|
||||
res = self.generate_apphub(get)
|
||||
if not res["status"]:
|
||||
return public.return_message(-1, 0, res["msg"])
|
||||
#删除模板
|
||||
public.ExecShell("rm -rf {}".format(os.path.join(AppHub.hub_home_path, "templates")))
|
||||
|
||||
public.set_module_logs('apphub', 'import_git_apphub', 1)
|
||||
return public.return_message(0, 0, public.lang("Import from git successful"))
|
||||
|
||||
def import_zip_apphub(self, get):
|
||||
'''
|
||||
@name 从压缩到包导入外部应用
|
||||
@param get: sfile: zip文件路径
|
||||
'''
|
||||
sfile = get.sfile.strip()
|
||||
files = sfile.split(",")
|
||||
|
||||
for sfile in files:
|
||||
|
||||
if not sfile.endswith(('.zip', '.gz')):
|
||||
return public.return_message(-1, 0, public.lang("The file format is incorrect, please select the zip or gz file"))
|
||||
|
||||
if not os.path.exists(self.hub_home_path):
|
||||
os.makedirs(self.hub_home_path)
|
||||
|
||||
if sfile.endswith('.zip'):
|
||||
res, err = public.ExecShell("unzip -o {} -d {}".format(sfile, self.hub_home_path))
|
||||
elif sfile.endswith('.gz'):
|
||||
res, err = public.ExecShell("tar -xzvf {} -C {}".format(sfile, self.hub_home_path))
|
||||
else:
|
||||
err = "{},Unsupported file formats".format(sfile)
|
||||
|
||||
if err:
|
||||
return public.return_message(-1, 0, public.lang("Import failure:{}", str(err)))
|
||||
|
||||
res = self.generate_apphub(get)
|
||||
if not res["status"]:
|
||||
return public.return_message(-1, 0, res["msg"])
|
||||
|
||||
public.set_module_logs('apphub', 'import_zip_apphub', 1)
|
||||
|
||||
return public.return_message(0, 0, public.lang("Successful import"))
|
||||
|
||||
def parser_zip_apphub(self, get):
|
||||
'''
|
||||
@name 解析zip包
|
||||
@param get: sfile: zip文件路径
|
||||
@return app_list: 外部应用列表
|
||||
'''
|
||||
sfile = get.sfile.strip()
|
||||
|
||||
app_list = []
|
||||
|
||||
from mod.project.docker.apphub.tool import GzHandler, ZipHandler
|
||||
if sfile.endswith(".gz"):
|
||||
handler = GzHandler()
|
||||
else:
|
||||
handler = ZipHandler()
|
||||
|
||||
files = handler.get_files(sfile)
|
||||
|
||||
if 'status' in files:
|
||||
return public.return_message(-1, 0, files['msg'])
|
||||
|
||||
for file, file_struck in files.items():
|
||||
if 'app.json' in file_struck and file_struck['app.json']['is_dir'] == 0:
|
||||
|
||||
filename = file_struck['app.json']['fullpath']
|
||||
|
||||
appinfo = handler.get_file_info(sfile, filename)
|
||||
if 'status' in appinfo and appinfo['status'] == False:
|
||||
return public.return_message(-1, 0, appinfo['msg'])
|
||||
|
||||
try:
|
||||
appinfo = json.loads(appinfo['data'])
|
||||
appinfo["parser_from"] = sfile
|
||||
app_list.append(appinfo)
|
||||
except:
|
||||
pass
|
||||
|
||||
return app_list
|
||||
190
mod/project/docker/apphub/tool.py
Normal file
190
mod/project/docker/apphub/tool.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import datetime
|
||||
import tarfile
|
||||
import zipfile
|
||||
import public
|
||||
|
||||
|
||||
class BaseCompressHandler:
|
||||
"""压缩文件处理基类"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_files(self, sfile):
|
||||
"""获取压缩包内文件列表"""
|
||||
pass
|
||||
|
||||
def get_file_info(self, sfile,filename):
|
||||
"""获取压缩包内文件信息"""
|
||||
pass
|
||||
|
||||
def check_file_exists(self, file_path):
|
||||
"""检查文件是否存在"""
|
||||
if not os.path.exists(file_path):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class GzHandler(BaseCompressHandler):
|
||||
"""tar.gz压缩文件处理类"""
|
||||
|
||||
def get_filename(self, item):
|
||||
"""获取压缩包文件名"""
|
||||
filename = item.name
|
||||
try:
|
||||
filename = item.name.encode('cp437').decode('gbk')
|
||||
except:
|
||||
pass
|
||||
if item.isdir():
|
||||
filename += '/'
|
||||
return filename
|
||||
|
||||
def check_file_type(self, file_path):
|
||||
"""检查文件是否为tar文件"""
|
||||
if not tarfile.is_tarfile(file_path):
|
||||
if file_path[-3:] == ".gz":
|
||||
return False, 'This is not tar.gz archive file, the gz archive file does not support preview, only decompression'
|
||||
return False, 'Not a valid tar.gz archive file'
|
||||
return True, ''
|
||||
|
||||
def get_files(self, sfile):
|
||||
"""获取压缩包内文件列表"""
|
||||
if not self.check_file_exists(sfile):
|
||||
return public.returnMsg(False, 'FILE_NOT_EXISTS')
|
||||
|
||||
is_valid, message = self.check_file_type(sfile)
|
||||
if not is_valid:
|
||||
return public.returnMsg(False, message)
|
||||
|
||||
zip_file = tarfile.open(sfile)
|
||||
data = {}
|
||||
for item in zip_file.getmembers():
|
||||
file_name = self.get_filename(item)
|
||||
|
||||
temp_list = file_name.split("/")
|
||||
|
||||
sub_data = data
|
||||
for name in temp_list:
|
||||
if not name: continue
|
||||
if name not in sub_data:
|
||||
if file_name.endswith(name) and not ".{}".format(name) in file_name:
|
||||
sub_data[name] = {
|
||||
'file_size': item.size,
|
||||
'filename': name,
|
||||
'fullpath': file_name,
|
||||
'date_time': public.format_date(times=item.mtime),
|
||||
'is_dir': 1 if item.isdir() else 0
|
||||
}
|
||||
else:
|
||||
sub_data[name] = {}
|
||||
sub_data = sub_data[name]
|
||||
|
||||
zip_file.close()
|
||||
return data
|
||||
|
||||
def get_file_info(self, sfile, filename):
|
||||
"""获取压缩包内文件信息"""
|
||||
if not self.check_file_exists(sfile):
|
||||
return public.returnMsg(False, 'FILE_NOT_EXISTS')
|
||||
|
||||
tmp_path = '{}/tmp/{}'.format(public.get_panel_path(), public.md5(sfile + filename))
|
||||
result = {}
|
||||
result['status'] = True
|
||||
result['data'] = ''
|
||||
with tarfile.open(sfile, 'r') as zip_file:
|
||||
try:
|
||||
zip_file.extract(filename, tmp_path)
|
||||
result['data'] = public.readFile('{}/{}'.format(tmp_path, filename))
|
||||
except:
|
||||
pass
|
||||
public.ExecShell("rm -rf {}".format(tmp_path))
|
||||
return result
|
||||
|
||||
|
||||
class ZipHandler(BaseCompressHandler):
|
||||
"""zip压缩文件处理类"""
|
||||
|
||||
def check_file_type(self, sfile, is_close=False):
|
||||
"""检查文件是否为zip文件"""
|
||||
zip_file = None
|
||||
try:
|
||||
zip_file = zipfile.ZipFile(sfile)
|
||||
except:
|
||||
pass
|
||||
|
||||
if is_close and zip_file:
|
||||
zip_file.close()
|
||||
|
||||
return zip_file
|
||||
|
||||
def get_filename(self, item):
|
||||
"""获取压缩包文件名"""
|
||||
path = item.filename
|
||||
try:
|
||||
path_name = path.encode('cp437').decode('utf-8')
|
||||
except:
|
||||
try:
|
||||
path_name = path.encode('cp437').decode('gbk')
|
||||
path_name = path_name.encode('utf-8').decode('utf-8')
|
||||
except:
|
||||
path_name = path
|
||||
|
||||
return path_name
|
||||
|
||||
def get_files(self, sfile):
|
||||
"""获取压缩包内文件列表"""
|
||||
if not self.check_file_exists(sfile):
|
||||
return public.returnMsg(False, 'FILE_NOT_EXISTS')
|
||||
|
||||
zip_file = self.check_file_type(sfile)
|
||||
if not zip_file:
|
||||
return public.returnMsg(False, 'NOT_ZIP_FILE')
|
||||
|
||||
data = {}
|
||||
for item in zip_file.infolist():
|
||||
file_name = self.get_filename(item)
|
||||
|
||||
temp_list = file_name.lstrip("./").split("/")
|
||||
|
||||
sub_data = data
|
||||
for name in temp_list:
|
||||
if not name: continue
|
||||
if name not in sub_data:
|
||||
if file_name.endswith(name):
|
||||
sub_data[name] = {
|
||||
'file_size': item.file_size,
|
||||
'compress_size': item.compress_size,
|
||||
'compress_type': item.compress_type,
|
||||
'filename': name,
|
||||
'fullpath': file_name,
|
||||
'date_time': datetime.datetime(*item.date_time).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
'is_dir': 1 if item.is_dir() else 0
|
||||
}
|
||||
else:
|
||||
sub_data[name] = {}
|
||||
sub_data = sub_data[name]
|
||||
|
||||
zip_file.close()
|
||||
return data
|
||||
|
||||
def get_file_info(self, sfile,filename):
|
||||
"""获取压缩包内文件信息"""
|
||||
if not self.check_file_exists(sfile):
|
||||
return public.returnMsg(False, 'FILE_NOT_EXISTS')
|
||||
|
||||
result = {}
|
||||
result['status'] = True
|
||||
result['data'] = ''
|
||||
with zipfile.ZipFile(sfile, 'r') as zip_file:
|
||||
for item in zip_file.infolist():
|
||||
z_filename = self.get_filename(item)
|
||||
if z_filename == filename:
|
||||
buff = zip_file.read(item.filename)
|
||||
encoding, srcBody = public.decode_data(buff)
|
||||
result['encoding'] = encoding
|
||||
result['data'] = srcBody
|
||||
break
|
||||
return result
|
||||
397
mod/project/docker/comMod.py
Normal file
397
mod/project/docker/comMod.py
Normal file
@@ -0,0 +1,397 @@
|
||||
# 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
|
||||
747
mod/project/docker/composeMod.py
Normal file
747
mod/project/docker/composeMod.py
Normal file
@@ -0,0 +1,747 @@
|
||||
# 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())
|
||||
0
mod/project/docker/docker_compose/__init__.py
Normal file
0
mod/project/docker/docker_compose/__init__.py
Normal file
317
mod/project/docker/docker_compose/base.py
Normal file
317
mod/project/docker/docker_compose/base.py
Normal file
@@ -0,0 +1,317 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# YakPanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2015-2099 YakPanel(www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: wzz <wzz@yakpanel.com>
|
||||
# -------------------------------------------------------------------
|
||||
# ------------------------------
|
||||
# docker模型 - docker compose 基类
|
||||
# ------------------------------
|
||||
import sys
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
import public
|
||||
|
||||
|
||||
class Compose():
|
||||
|
||||
def __init__(self):
|
||||
self.cmd = 'docker-compose'
|
||||
self.path = None
|
||||
self.tail = "100"
|
||||
self.type = 0
|
||||
self.compose_name = None
|
||||
self.compose_project_path = "{}/data/compose".format(public.get_panel_path())
|
||||
self.grep_version = 'grep -v "\`version\` is obsolete"'
|
||||
self.def_name = None
|
||||
self.container_id = None
|
||||
self.ps_count = 0
|
||||
|
||||
def set_container_id(self, container_id: str) -> 'Compose':
|
||||
self.container_id = container_id
|
||||
return self
|
||||
|
||||
def get_cmd(self) -> str:
|
||||
return self.cmd
|
||||
|
||||
def set_cmd(self, cmd: str) -> 'Compose':
|
||||
self.cmd = cmd
|
||||
return self
|
||||
|
||||
def set_path(self, path: str, rep: bool = False) -> 'Compose':
|
||||
if rep:
|
||||
self.path = path.replace("\'", "\\'").replace("\"", "\\\"").replace(" ", "\\ ").replace("|", "\\|")
|
||||
else:
|
||||
self.path = path
|
||||
return self
|
||||
|
||||
def set_tail(self, tail: str) -> 'Compose':
|
||||
self.tail = tail
|
||||
return self
|
||||
|
||||
def set_type(self, type: int) -> 'Compose':
|
||||
self.type = type
|
||||
return self
|
||||
|
||||
def set_compose_name(self, compose_name: str) -> 'Compose':
|
||||
self.compose_name = compose_name
|
||||
return self
|
||||
|
||||
def set_def_name(self, def_name: str) -> 'Compose':
|
||||
self.def_name = def_name
|
||||
return self
|
||||
|
||||
def get_compose_up(self) -> List[str] or str:
|
||||
return self.cmd + ' -f {} up -d| {}'.format(self.path, self.grep_version)
|
||||
|
||||
def get_compose_up_remove_orphans(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} up -d --remove-orphans'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'up', '-d', '--remove-orphans']
|
||||
|
||||
def get_compose_down(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} down'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'down']
|
||||
|
||||
def kill_compose(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} kill'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'kill']
|
||||
|
||||
def rm_compose(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} rm -f'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'rm', '-f']
|
||||
|
||||
def get_compose_delete(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} down --volumes --remove-orphans'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'down', '--volumes', '--remove-orphans']
|
||||
|
||||
def get_compose_delete_for_ps(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -p {} down --volumes --remove-orphans'.format(self.compose_name)
|
||||
else:
|
||||
return [self.cmd, '-p', self.compose_name, 'down', '--volumes', '--remove-orphans']
|
||||
|
||||
def get_compose_restart(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} restart'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'restart']
|
||||
|
||||
def get_compose_stop(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} stop'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'stop']
|
||||
|
||||
def get_compose_start(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} start'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'start']
|
||||
|
||||
def get_compose_pull(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} pull'.format(self.path)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'pull']
|
||||
|
||||
def get_compose_logs(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} logs -f --tail {}'.format(self.path, self.tail)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'logs', '-f', '--tail', self.tail]
|
||||
|
||||
def get_tail_compose_log(self) -> List[str] or str:
|
||||
if self.type == 0:
|
||||
return self.cmd + ' -f {} logs --tail {}'.format(self.path, self.tail)
|
||||
else:
|
||||
return [self.cmd, '-f', self.path, 'logs', '--tail', self.tail]
|
||||
|
||||
def get_compose_ls(self) -> List[str] or str:
|
||||
return self.cmd + ' ls -a --format json| {}'.format(self.grep_version)
|
||||
|
||||
def get_compose_ps(self) -> List[str] or str:
|
||||
return self.cmd + ' -f {} ps -a --format json| {}'.format(self.path, self.grep_version)
|
||||
|
||||
def get_compose_config(self) -> List[str] or str:
|
||||
return self.cmd + ' -f {} config| {}'.format(self.path, self.grep_version)
|
||||
|
||||
def get_container_logs(self) -> List[str] or str:
|
||||
# return ['docker', 'logs', '-f', self.container_id]
|
||||
return "docker logs {} --tail {} 2>&1".format(self.container_id, self.tail)
|
||||
|
||||
def wsResult(self, status: bool = True, msg: str = "", data: any = None, timestamp: int = None, code: int = 0,
|
||||
args: any = None):
|
||||
# public.print_log("wsResult code -- {} status--{}".format(code, status))
|
||||
# rs = public.returnResult(status, msg, data, timestamp, code, args)
|
||||
|
||||
import time
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
if msg is None:
|
||||
msg = "OK"
|
||||
rs = {"code": code, "status": status, "msg": msg, "data": data, "timestamp": timestamp,
|
||||
"def_name": self.def_name}
|
||||
|
||||
# public.print_log("wsResult rs -- {} ".format(rs))
|
||||
return rs
|
||||
|
||||
# 2024/8/1 下午3:29 构造分页数据
|
||||
def get_page(self, data, get):
|
||||
get.row = get.get("row", 20)
|
||||
# get.row = 20000
|
||||
get.p = get.get("p", 1)
|
||||
import page
|
||||
page = page.Page()
|
||||
info = {'count': len(data), 'row': int(get.row), 'p': int(get.p), 'uri': {}, 'return_js': ''}
|
||||
|
||||
result = {'page': page.GetPage(info)}
|
||||
n = 0
|
||||
result['data'] = []
|
||||
for i in range(info['count']):
|
||||
if n >= page.ROW: break
|
||||
if i < page.SHIFT: continue
|
||||
n += 1
|
||||
result['data'].append(data[i])
|
||||
return result
|
||||
|
||||
# 2024/7/29 下午4:22 检查web服务是否正常
|
||||
def check_web_status(self):
|
||||
'''
|
||||
@name 检查web服务是否正常
|
||||
@param "data":{"参数名":""} <数据类型> 参数描述
|
||||
@return dict{"status":True/False,"msg":"提示信息"}
|
||||
'''
|
||||
from mod.base.web_conf import util
|
||||
webserver = util.webserver()
|
||||
if webserver != "nginx" or webserver is None:
|
||||
return public.returnResult(status=False, msg="Domain name access only supports Nginx. Please go to the software store to install Nginx or choose not to use domain name access!")
|
||||
|
||||
from panelSite import panelSite
|
||||
site_obj = panelSite()
|
||||
site_obj.check_default()
|
||||
|
||||
wc_err = public.checkWebConfig()
|
||||
if not wc_err:
|
||||
return public.returnResult(
|
||||
status=False,
|
||||
msg='ERROR: An error in the configuration file has been detected. Please eliminate it before proceeding. <br><br><a style="color:red;">' +
|
||||
wc_err.replace("\n", '<br>') + '</a>'
|
||||
)
|
||||
|
||||
return public.return_message(0, 0, '')
|
||||
def pageResult(self, status: bool = True,
|
||||
msg: str = "",
|
||||
data: any = None,
|
||||
timestamp: int = None,
|
||||
code: int = 0,
|
||||
args: any = None,
|
||||
page: any = None,
|
||||
cpu: any = None,
|
||||
mem: any = None):
|
||||
# rs = public.returnResult(status, msg, data, timestamp, code, args)
|
||||
# public.print_log("re 列表 --{}".format(rs['msg']))
|
||||
# import time
|
||||
# if timestamp is None:
|
||||
# timestamp = int(time.time())
|
||||
if msg is None:
|
||||
msg = "OK"
|
||||
# rs = {"code": code, "status": status, "msg": msg, "data": data, "timestamp": timestamp}
|
||||
rs = {"msg": msg, "data": data}
|
||||
if not self.def_name is None:
|
||||
rs["def_name"] = self.def_name
|
||||
if not cpu is None:
|
||||
rs["maximum_cpu"] = cpu
|
||||
if not mem is None:
|
||||
rs["maximum_memory"] = mem
|
||||
if not page is None:
|
||||
rs["page"] = page
|
||||
st = 0 if status else -1
|
||||
return public.return_message(st, 0, rs)
|
||||
|
||||
# 2024/6/25 下午2:40 获取日志类型的websocket返回值
|
||||
def exec_logs(self, get, command, cwd=None):
|
||||
'''
|
||||
@name 获取日志类型的websocket返回值
|
||||
@author wzz <2024/6/25 下午2:41>
|
||||
@param "data":{"参数名":""} <数据类型> 参数描述
|
||||
@return dict{"status":True/False,"msg":"提示信息"}
|
||||
'''
|
||||
import json,select
|
||||
if self.def_name is None: self.set_def_name(get.def_name)
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
if not hasattr(get, '_ws'):
|
||||
return
|
||||
|
||||
p = Popen(command, stdout=PIPE, stderr=STDOUT, cwd=cwd)
|
||||
try:
|
||||
while True:
|
||||
# 优先检查连接状态,如果断开则立即停止
|
||||
if not get._ws.connected:
|
||||
break
|
||||
|
||||
# 检查是否有数据可读
|
||||
readable, _, _ = select.select([p.stdout], [], [], 0.01)
|
||||
|
||||
if p.stdout in readable:
|
||||
line = p.stdout.readline()
|
||||
if line:
|
||||
try:
|
||||
get._ws.send(json.dumps(self.wsResult(True, "{}".format(line.decode('utf-8').rstrip()))))
|
||||
except:
|
||||
break
|
||||
elif p.poll() is not None:
|
||||
# 没有数据可读,且进程已结束,则退出
|
||||
break
|
||||
|
||||
finally:
|
||||
if p.poll() is None:
|
||||
p.kill()
|
||||
|
||||
|
||||
# 2024/6/25 下午2:40 获取日志类型的websocket返回值
|
||||
def status_exec_logs(self, get, command, cwd=None):
|
||||
'''
|
||||
@name 获取日志类型的websocket返回值
|
||||
@author wzz <2024/6/25 下午2:41>
|
||||
@param "data":{"参数名":""} <数据类型> 参数描述
|
||||
@return dict{"status":True/False,"msg":"提示信息"}
|
||||
'''
|
||||
import json
|
||||
if self.def_name is None: self.set_def_name(get.def_name)
|
||||
from subprocess import Popen, PIPE, STDOUT
|
||||
|
||||
p = Popen(command, stdout=PIPE, stderr=STDOUT, cwd=cwd)
|
||||
|
||||
while True:
|
||||
if p.poll() is not None:
|
||||
break
|
||||
|
||||
line = p.stdout.readline() # 非阻塞读取
|
||||
if line:
|
||||
try:
|
||||
if hasattr(get, '_ws'):
|
||||
get._ws.send(json.dumps(self.wsResult(
|
||||
True,
|
||||
"{}\r\n".format(line.decode('utf-8').rstrip()),
|
||||
)))
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
65
mod/project/docker/gpuMod.py
Normal file
65
mod/project/docker/gpuMod.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import sys
|
||||
|
||||
from mod.project.docker.app.gpu import nvidia
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.append('/www/server/panel/class')
|
||||
|
||||
import public
|
||||
|
||||
def gpu_class():
|
||||
return 'nvidia'
|
||||
|
||||
|
||||
class main:
|
||||
def __init__(self):
|
||||
self.driver = None
|
||||
if gpu_class() == 'nvidia':
|
||||
self.driver = nvidia.NVIDIA()
|
||||
# elif gpu_class() == 'amd':
|
||||
# self.driver = amd.AMD()
|
||||
|
||||
def get_all_device_info(self, get):
|
||||
"""
|
||||
获取所有gpu信息
|
||||
Args:
|
||||
get:
|
||||
|
||||
Returns:
|
||||
dict: All gpu information are included.
|
||||
"""
|
||||
public.print_log('gpu info')
|
||||
if not self.driver.support:
|
||||
return public.return_message(0, 0, {})
|
||||
return public.return_message(0, 0, self.driver.get_all_device_info())
|
||||
|
||||
def get_info_by_index(self, get):
|
||||
"""
|
||||
返回驱动信息
|
||||
Args:
|
||||
get:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
index = 0
|
||||
if not self.driver.support:
|
||||
return public.return_message(0, 0, {})
|
||||
try:
|
||||
index = int(get.index)
|
||||
except ValueError as e:
|
||||
public.returnResult(False, "{} need an int: {}".format(self.get_info_by_index.__name__, e))
|
||||
return public.return_message(0, 0, self.driver.get_info_by_index(index))
|
||||
|
||||
def get_system_info(self, get):
|
||||
"""
|
||||
返回驱动信息
|
||||
Args:
|
||||
get:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if not self.driver.support:
|
||||
return public.return_message(0, 0, {})
|
||||
return public.return_message(0, 0, self.driver.get_system_info())
|
||||
0
mod/project/docker/proxy/__init__.py
Normal file
0
mod/project/docker/proxy/__init__.py
Normal file
3426
mod/project/docker/proxy/base.py
Normal file
3426
mod/project/docker/proxy/base.py
Normal file
File diff suppressed because it is too large
Load Diff
65
mod/project/docker/routetestMod.py
Normal file
65
mod/project/docker/routetestMod.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# coding: utf-8
|
||||
# -------------------------------------------------------------------
|
||||
# yakpanel
|
||||
# -------------------------------------------------------------------
|
||||
# Copyright (c) 2015-2099 yakpanel(http://www.yakpanel.com) All rights reserved.
|
||||
# -------------------------------------------------------------------
|
||||
# Author: wzz <wzz@yakpanel.com>
|
||||
# -------------------------------------------------------------------
|
||||
import os
|
||||
# ------------------------------
|
||||
# Docker模型
|
||||
# ------------------------------
|
||||
import sys
|
||||
|
||||
if "/www/server/panel/class" not in sys.path:
|
||||
sys.path.insert(0, "/www/server/panel/class")
|
||||
|
||||
os.chdir("/www/server/panel")
|
||||
import public
|
||||
|
||||
|
||||
class main():
|
||||
|
||||
def returnResult(self, get):
|
||||
'''
|
||||
@name 模型测试方法,请求方式
|
||||
/mod/docker/routetestMod/returnResult
|
||||
支持form-data和json
|
||||
|
||||
使用通用的响应对象,返回json格式数据
|
||||
@author wzz <2024/2/19 上午 10:37>
|
||||
@param "data":{"参数名":""} <数据类型> 参数描述
|
||||
@return dict{"status":True/False,"msg":"提示信息"}
|
||||
'''
|
||||
print(public.returnResult(msg="hello"))
|
||||
return public.returnResult(msg="hello")
|
||||
|
||||
def wsRequest(self, get):
|
||||
"""
|
||||
处理websocket,ws测试方法,请求方式
|
||||
ws://192.168.x.x:8888/ws_mod
|
||||
连接成功后先发送第一条信息{"x-http-token":"token"}
|
||||
然后再发第二条信息,信息内容如下格式
|
||||
|
||||
备注:如果需要使用apipost测试,请将__init__.py中ws模型路由的comReturn和csrf检查注释掉再测试
|
||||
@param get:
|
||||
{"mod_name":"docker","sub_mod_name":"routetest","def_name":"wsRequest","ws_callback":"111"}
|
||||
{"mod_name":"模型名称","sub_mod_name":"子模块名称","def_name":"函数名称","ws_callback":"ws必传参数,传111",其他参数接后面}
|
||||
@return:
|
||||
"""
|
||||
if not hasattr(get, "_ws"):
|
||||
return True
|
||||
|
||||
import time
|
||||
sum = 0
|
||||
while sum < 10:
|
||||
time.sleep(0.2)
|
||||
get._ws.send("hello\r\n")
|
||||
sum += 1
|
||||
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main().returnResult({})
|
||||
Reference in New Issue
Block a user