318 lines
12 KiB
Python
318 lines
12 KiB
Python
|
|
# 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
|