# coding: utf-8 # ------------------------------------------------------------------- # YakPanel # ------------------------------------------------------------------- # Copyright (c) 2014-2099 YakPanel(www.yakpanel.com) All rights reserved. # ------------------------------------------------------------------- # Author: wzz # ------------------------------------------------------------------- # ------------------------------ # Docker模型 # ------------------------------ import public import os import time import json import re from btdockerModelV2 import dk_public as dp from btdockerModelV2 import setupModel as ds from btdockerModelV2 import volumeModel as dv from btdockerModelV2.dockerBase import dockerBase from public.validate import Param class main(dockerBase): compose_path = "{}/data/compose".format(public.get_panel_path()) project_path = "/www/dk_project" templates_path = "{}/templates".format(project_path) config_path = "{}/config".format(public.get_panel_path()) info_path = "{}/docker_project_info.json".format(config_path) __first_pl = "{}/first.pl".format(project_path) def __init__(self): self.log_file = "/tmp/dk_project_run.log" self.docker_setup = ds.main() if not os.path.exists(self.templates_path): os.system("mkdir -p {}".format(self.templates_path)) self.compose_cmd = "/usr/bin/docker-compose" if self.docker_setup.check_docker_compose_service()[0] \ else "/usr/local/bin/docker-compose" def __check_conf(self, filename): ''' 验证配置文件是否可执行 @param filename: docker-compose.yml文件路劲 @return: ''' return public.ExecShell("{} -f {} config".format(self.compose_cmd, filename)) def sync_item(self, get): ''' 同步官方可以一键部署的项目 @param get: 空对象 @return: ''' os.remove(self.info_path) project_info = self._get_project_list(get) failed_list = [] successes_list = [] for info in project_info: if info["server_name"]: down_project_yml = self.__download_project_yml(info["server_name"]) if not down_project_yml["status"]: failed_list.append(info["server_name"]) continue successes_list.append(info["server_name"]) data = [{"successes": len(successes_list), "server_name": successes_list}, {"failed": len(failed_list), "server_name": failed_list}] return public.return_message(0, 0, data) def __first_sync_item(self, project_info): ''' 同步官方可以一键部署的项目 @param get: 空对象 @return: ''' failed_list = [] successes_list = [] for info in project_info: if info["server_name"]: down_project_yml = self.__download_project_yml(info["server_name"]) if not down_project_yml["status"]: failed_list.append(info["server_name"]) continue successes_list.append(info["server_name"]) data = [{"successes": len(successes_list), "server_name": successes_list}, {"failed": len(failed_list), "server_name": failed_list}] return data def get_project_list(self, get): ''' 获取支持一键部署的项目列表 @param get: @return: ''' project_info = self._get_project_list(get) return public.return_message(0, 0, project_info) def _get_project_list(self, get): ''' 获取支持一键部署的项目列表 @param get: @return: ''' project_info = [] try: if not os.path.exists(self.info_path): down_info = self.__download_info(self.info_path) if not down_info["status"]: return project_info project_info = json.loads(public.readFile(self.info_path)) project_info.sort(key=lambda x: x["sort"]) if not os.path.exists(self.__first_pl): sync_result = self.__first_sync_item(project_info) for result in sync_result: if result.get("successes") and result["successes"] <= 0: return project_info public.ExecShell("echo \"first\" > {}".format(self.__first_pl)) except Exception as e: project_info = [] return project_info def __get_docker_status(self, args): ''' 获取docker安装和启动状态 @param args: @return: ''' return { "installed": self.docker_setup.check_docker_compose_service(), "service_status": self.docker_setup.get_service_status() } def __download_info(self, info_path): ''' 下载版本信息: info.json @param info_path: string info.json文件的路劲 @return: ''' url = "{}/install/lib/docker_project/docker_project_info.json".format(public.get_url()) dp.download_file(url, info_path) if os.path.exists(info_path): return public.return_message(0, 0, public.lang("info.json is downloaded!")) return public.return_message(-1, 0, public.lang("The info.json download failed!")) def __download_project_yml(self, server_name): ''' 下载指定项目压缩包 @param server_name: string 模板名称,如nextcloud @return: ''' try: path = "{}/{}".format(self.templates_path, server_name) filename = "{}/{}.tar.gz".format(self.templates_path, server_name) compose_file = "{}/docker-compose.yml".format(path) url = "{}/install/lib/docker_project/templates/{}.tar.gz".format(public.get_url(), server_name) dp.download_file(url, filename) if not os.path.exists(filename): return public.return_message(-1, 0, public.lang("{} Download failed, please resync!", server_name)) if os.path.getsize(filename) == 0: os.remove(filename) return public.return_message(-1, 0, public.lang("{} Download failed, please resync!", server_name)) self.__tar_x_yml(server_name, path, filename) if os.path.exists(compose_file): check_conf = self.__check_conf(compose_file) if check_conf[1]: return public.return_message(-1, 0, public.lang("{}yml file test failed,{}", server_name, check_conf[1])) return public.return_message(0, 0, public.lang("{} Download completed!", server_name)) except: return public.return_message(-1, 0, public.lang("{} Download failed, please resync!", server_name)) def __tar_x_yml(self, server_name, path=None, filename=None): ''' 解压项目模板方法 @param server_name: 模板名称,如nextcloud @param path: 项目模板路劲,如/www/dk_project/templates/nextcloud @param filename: 项目模板压缩包,如/www/dk_project/templates/nextcloud.tar.gz @return: ''' tar_result = public.ExecShell("tar xvf {} -C {}".format(filename, self.templates_path)) if tar_result[1]: os.remove(path) os.remove(filename) return public.return_message(-1, 0, public.lang("{} Decompression failed", server_name)) return public.return_message(0, 0, public.lang("{} extracted successfully", server_name)) def create_project_volume(self, server_name, project_name, dir_names, volume_path): ''' 创建指定项目的数据存储卷 @param volume_path: @param project_name: string @param dir_names: list [dir_name,dir_name,...] @return: ''' args = public.dict_obj() args.url = "unix:///var/run/docker.sock" # volumes = dv.main().get_volume_list(args) # {'status': True, 'msg': {'volume': [], 'installed': True, 'service_status': True}} # if volumes['status']: # volumes = volumes['msg']['volume'] # else: # volumes = list() # volume的值,一个list: [] for dir_name in dir_names: # # 如果已经存在就跳过 # for volume in volumes: # if dir_name == volume["Name"]: # continue if volume_path == "": path = "{}/projects/{}/data/{}".format(self.project_path, project_name, dir_name) else: path = "{}/data/{}".format(volume_path, dir_name) is_mkdir = public.ExecShell("mkdir -p {}".format(path)) if is_mkdir[1]: return public.return_message(-1, 0, public.lang("Directory creation failed for the following reasons: {}", is_mkdir[1])) args.name = "{}_{}_{}".format(project_name, server_name, dir_name) args.driver = "local" args.driver_opts = {'type': 'none', 'device': path, 'o': 'bind'} args.labels = {} dv.main().add(args) return public.return_message(0, 0, public.lang("The storage volume has been created")) def get_project(self, get): ''' 获取指定一键部署项目的配置信息 @param get: get.server_name @return: ''' # 校验参数 try: get.validate([ Param('server_name').Require().String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) try: server_name = getattr(get, "server_name") info_path = "{}/{}/conf.json".format(self.templates_path, server_name) project_info = json.loads(public.readFile(info_path)) volume_placeholder = "Default: {}/projects/ your project name /data/".format(self.project_path) total_sum = len(project_info) volume_path = {"id": total_sum + 1, "sort": total_sum + 1, "type": "string", "key": "VOLUME_PATH", "value": "", "placeholder": volume_placeholder, "ps": "Data storage directory"} project_info.append(volume_path) except: project_info = [] return public.return_message(0, 0, project_info) def _get_project(self, get): ''' 获取指定一键部署项目的配置信息 @param get: get.server_name @return: ''' try: server_name = getattr(get, "server_name") info_path = "{}/{}/conf.json".format(self.templates_path, server_name) project_info = json.loads(public.readFile(info_path)) volume_placeholder = "Default: {}/projects/ your project name /data/".format(self.project_path) total_sum = len(project_info) volume_path = {"id": total_sum + 1, "sort": total_sum + 1, "type": "string", "key": "VOLUME_PATH", "value": "", "placeholder": volume_placeholder, "ps": "Data storage directory"} project_info.append(volume_path) except: project_info = [] return project_info def __get_server_ps(self, project_conf, conf_key): ''' 获取对应服务名的标题 @param project_conf: @param conf_key: @return: ''' get = public.dict_obj() for conf in project_conf: if conf["key"] == "SERVER_NAME": get.server_name = conf["value"] server_conf = self._get_project(get) for server in server_conf: if conf_key == server["key"]: return server["ps"] return conf_key def get_project_logs(self, get): """ 获取一键部署日志,websocket @param get: @return: """ get.wsLogTitle = "Please wait to execute the command..." print(self.log_file) get._log_path = self.log_file return self.get_ws_log(get) def create_project(self, get): ''' 创建一键部署的项目 @param get: @return: ''' # {"project_conf": [{"key": "PROJECT_NAME", "value": "sdfasdf"}, {"key": "PORT", "value": "8180"}, # {"key": "DB_ROOT_PASS", "value": "bt_nextcloud"}, {"key": "DB_NAME", "value": "nextcloud"}, # {"key": "DB_USER", "value": "nextcloud"}, {"key": "DB_PASS", "value": "bt_nextcloud"}, # {"key": "VOLUME_PATH", "value": "/www/dk_project/projects/sdfasdf"}, # {"key": "REMARK", "value": "SDFADSF"}, {"key": "SERVER_NAME", "value": "nextcloud"}, # {"key": "VOLUMES", "value": ["nextcloud", "db"]}]} # 校验参数 try: get.validate([ Param('project_conf').Require().List(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) project_conf = getattr(get, "project_conf") remark = "" for conf in project_conf: if conf["key"] != "REMARK" and type(conf["value"]) != list: if re.search(r'\s', conf["value"]): server_ps = self.__get_server_ps(project_conf, conf["key"]) return public.return_message(-1, 0, public.lang("{} cannot contain Spaces", server_ps)) if conf["key"] != "VOLUME_PATH" and conf["key"] != "REMARK": if conf["value"] == "": server_ps = self.__get_server_ps(project_conf, conf["key"]) return public.return_message(-1, 0, public.lang("{} cannot be null!", server_ps)) if conf["key"].upper() == "PROJECT_NAME": project_name = conf["value"].strip() if conf["key"].upper() == "VOLUME_PATH": project_volume = conf["value"].strip() if conf["key"].upper() == "SERVER_NAME": server_name = conf["value"].strip() if conf["key"].upper() == "VOLUMES": # VOLUMES = list volumes = conf["value"] if conf["key"].upper() == "PORT": if dp.check_socket(conf["value"]): return public.return_message(-1, 0, public.lang("Server port [ {}] is occupied, please change to another port!", conf['value'])) project_port = conf["value"] if conf["key"] == "REMARK": remark = conf["value"] config_path = "{}/config/name_map.json".format(public.get_panel_path()) if not os.path.exists(config_path): public.writeFile(config_path, json.dumps({})) if public.readFile(config_path) == '': public.writeFile(config_path, json.dumps({})) name_map = json.loads(public.readFile(config_path)) name_str = 'q18q' + public.GetRandomString(10).lower() name_map[name_str] = project_name project_name = name_str public.writeFile(config_path, json.dumps(name_map)) server_dir = "{}/{}".format(self.templates_path, server_name) project_dir = "{}/projects/{}/{}_{}".format(self.project_path, project_name, project_name, server_name) public.set_module_logs('docker_project', 'create_project', 1) check_result = self.__create_dir(project_dir, project_name, server_name, server_dir) # todo 修改返回内容 只取msg 测试是否取到 if not check_result["status"]: return public.return_message(-1, 0, check_result["msg"]) self.__write_config(project_dir, project_name, server_name, project_conf) self.create_project_volume(server_name, project_name, volumes, project_volume) run_result = self.__project_run(project_dir, project_name) if run_result["status"]: self.__add_sql(project_dir, project_name, server_name, remark) dp.write_log("One-click deployment project [{}] successful!".format(server_name)) return public.return_message(-1, 0, self.__return_msg(project_port)) return public.return_message(-1, 0, run_result) # return public.return_message(-1, 0, run_result["msg"]) def __project_run(self, project_dir, project_name): ''' 运行项目 @param project_dir: 项目运行目录 @param server_name: 服务名称 @return: ''' filename = "{}/docker-compose.yml".format(project_dir) check_result = self.__check_conf(filename) if check_result[1]: return public.return_message(-1, 0, public.lang("Project startup failed {}", check_result[1])) public.ExecShell("echo -n > {}".format(self.log_file)) public.ExecShell("nohup {} -f {}/docker-compose.yml up -d >> {} 2>&1 &&" " echo 'bt_successful' >> {} || echo 'bt_failed' >> {} &" .format( self.compose_cmd, project_dir, self.log_file, self.log_file, self.log_file )) return public.return_message(0, 0, public.lang("Start creating the project")) def __create_dir(self, project_dir, project_name, server_name, server_dir): ''' 创建项目目录 @param project_dir: 项目目录 @param project_name: 项目名称 @param server_dir: 服务源目录 @return: ''' if self.__check_repeat(project_dir, project_name, server_name): return public.return_message(-1, 0, public.lang("{} already exists, please change the project name", project_name)) mk_result = public.ExecShell("mkdir -p {}".format(project_dir)) if mk_result[1]: return public.return_message(-1, 0, public.lang("User project directory failed to create,details: {}", mk_result[1])) cp_result = public.ExecShell("cp -a {}/. {}/".format(server_dir, project_dir)) if cp_result[1]: return public.return_message(-1, 0, public.lang("Failed to copy project directory. Details: {}", cp_result[1])) return public.return_message(0, 0, public.lang("")) def __add_sql(self, project_dir, project_name, server_name, remark): ''' 添加项目到docker数据库中 @param project_dir: 项目路劲 @param project_name: 项目名称 @return: ''' pdata = { "name": public.xsssec("{}_{}".format(project_name, server_name)), "status": "1", "path": "{}/docker-compose.yml".format(project_dir), "template_id": "", "time": time.time(), "remark": public.xsssec(remark) } dp.sql("stacks").insert(pdata) def __return_msg(self, project_port): ''' 创建成功后返回给用户的数据 @param project_port: @return: ''' server_ip = public.get_server_ip() local_ip = public.GetLocalIp() data = {"protocol": "http", "server_ip": server_ip, "local_ip": local_ip, "port": project_port} return public.return_message(0, 0, data) def __check_repeat(self, project_dir, project_name, server_name): ''' 检查是否存在相同项目 @param project_dir: 项目路劲 @return: ''' # if os.path.exists(project_dir): # return True stacks_info = dp.sql("stacks").where("name=?", ("{}_{}".format(project_name, server_name),)).find() if stacks_info: return True return False def __write_config(self, project_dir, project_name, server_name, project_conf): ''' 写配置文件 @param project_dir: 用户项目目录 @param project_name: 项目名称 @param server_name: 服务名称,如nextcloud @param project_conf: 新的配置文件内容 @return: ''' old_env_path = "{}/{}/.env".format(self.templates_path, server_name) new_env_path = "{}/.env".format(project_dir) env_conf = "" if not os.path.exists(old_env_path): public.ExecShell("echo > {}".format(old_env_path)) with open(old_env_path) as env: lines = env.readlines() # 取旧文件转字典 old_dict = {} for line in lines: if "=" in line: temp = line.split("=") old_dict[temp[0]] = temp[1] # 新数据转字典 new_dict = {} for conf in project_conf: if conf["key"] == "VOLUME_PATH": project_volume = conf["value"] if "Default path" in project_volume: conf["value"] = "{}/{}/data/".format(self.project_path, project_name) continue if conf["key"] == "VOLUMES": continue new_dict[conf["key"].upper()] = conf["value"] # 旧字典更新新字典的内容 old_dict.update(new_dict) # 拼接成新的环境变量文件 for key, value in old_dict.items(): env_conf += "{}={}\n".format(key, value.strip()) public.writeFile(new_env_path, env_conf) return True def sync_compose_template(self, server_name): ''' 同步模板到项目模板页面 @param server_name: 模板名称 @return: ''' data = dp.sql("templates").where("name=?", (server_name,)).find() # if data: dp.sql("templates").delete(id=data["id"]) if data: return pdata = { "name": server_name, "remark": "YakPanel Docker Quick Deployment templates only [Do not delete them and use them separately to create projects]", "path": "{}/{}/docker-compose.yml".format(self.templates_path, server_name) } dp.sql("templates").insert(pdata) dp.write_log("Add template [{}] successful!".format(server_name))