manage.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. import os
  2. import io
  3. import sys
  4. import platform
  5. import shutil
  6. import time
  7. import subprocess
  8. import json
  9. import datetime
  10. import socket
  11. import re
  12. from threading import Thread
  13. from api.utils import shell_execute, docker, const
  14. from api.model.app import App
  15. from api.model.response import Response
  16. from api.model.config import Config
  17. from api.model.status_reason import StatusReason
  18. from api.utils.common_log import myLogger
  19. from redis import Redis
  20. from rq import Queue, Worker, Connection
  21. from rq.registry import StartedJobRegistry, FinishedJobRegistry, DeferredJobRegistry, FailedJobRegistry, ScheduledJobRegistry, CanceledJobRegistry
  22. from api.exception.command_exception import CommandException
  23. # 指定 Redis 容器的主机名和端口
  24. redis_conn = Redis(host='websoft9-redis', port=6379)
  25. # 使用指定的 Redis 连接创建 RQ 队列
  26. q = Queue(connection=redis_conn,default_timeout=3600)
  27. # APP列表是否有正在安装的应用
  28. def check_list_status(applist):
  29. installing = False
  30. for app in applist:
  31. status = app['status']
  32. if status == "created":
  33. installing = True
  34. break
  35. return installing
  36. # 获取所有app的信息
  37. def get_my_app(app_id):
  38. installed_list = get_apps_from_compose()
  39. installing_list = get_apps_from_queue()
  40. if check_list_status(installed_list):
  41. time.sleep(3)
  42. installed_list = get_apps_from_compose()
  43. app_list = installing_list + installed_list
  44. find = False
  45. ret = {}
  46. if app_id != None:
  47. for app in app_list:
  48. if app_id == app['app_id']:
  49. ret = app
  50. find = True
  51. break
  52. if not find:
  53. raise CommandException(const.ERROR_CLIENT_PARAM_NOTEXIST, "This App doesn't exist!", "")
  54. else:
  55. ret = app_list
  56. myLogger.info_logger("app list result ok")
  57. return ret
  58. # 获取具体某个app的信息
  59. def get_app_status(app_id):
  60. code, message = docker.check_app_id(app_id)
  61. if code == None:
  62. app = get_my_app(app_id)
  63. # 将app_list 过滤出app_id的app,并缩减信息,使其符合文档的要求
  64. ret = {}
  65. ret['app_id'] = app['app_id']
  66. ret['status'] = app['status']
  67. ret['status_reason'] = app['status_reason']
  68. else:
  69. raise CommandException(code, message, '')
  70. return ret
  71. def install_app(app_name, customer_name, app_version):
  72. myLogger.info_logger("Install app ...")
  73. ret = {}
  74. ret['ResponseData'] = {}
  75. app_id = app_name + "_" + customer_name
  76. ret['ResponseData']['app_id'] = app_id
  77. code, message = check_app(app_name, customer_name, app_version)
  78. if code == None:
  79. q.enqueue(install_app_delay, app_name, customer_name, app_version, job_id=app_id)
  80. else:
  81. ret['Error'] = get_error_info(code, message, "")
  82. return ret
  83. def start_app(app_id):
  84. code, message = docker.check_app_id(app_id)
  85. if code == None:
  86. info, flag = app_exits_in_docker(app_id)
  87. if flag:
  88. app_path = info.split()[-1].rsplit('/', 1)[0]
  89. cmd = "docker compose -f " + app_path + "/docker-compose.yml start"
  90. shell_execute.execute_command_output_all(cmd)
  91. else:
  92. raise CommandException(const.ERROR_CLIENT_PARAM_NOTEXIST, "APP is not exist", "")
  93. else:
  94. raise CommandException(code, message, '')
  95. def stop_app(app_id):
  96. code, message = docker.check_app_id(app_id)
  97. myLogger.info_logger(message)
  98. if code == None:
  99. info, flag = app_exits_in_docker(app_id)
  100. if flag:
  101. app_path = info.split()[-1].rsplit('/', 1)[0]
  102. cmd = "docker compose -f " + app_path + "/docker-compose.yml stop"
  103. shell_execute.execute_command_output_all(cmd)
  104. else:
  105. raise CommandException(const.ERROR_CLIENT_PARAM_NOTEXIST, "APP is not exist", "")
  106. else:
  107. myLogger.info_logger("check app failed")
  108. raise CommandException(code, message, "")
  109. def restart_app(app_id):
  110. code, message = docker.check_app_id(app_id)
  111. if code == None:
  112. info, flag = app_exits_in_docker(app_id)
  113. if flag:
  114. app_path = info.split()[-1].rsplit('/', 1)[0]
  115. cmd = "docker compose -f " + app_path + "/docker-compose.yml restart"
  116. shell_execute.execute_command_output_all(cmd)
  117. else:
  118. raise CommandException(const.ERROR_CLIENT_PARAM_NOTEXIST, "APP is not exist", "")
  119. else:
  120. raise CommandException(code, message, "")
  121. def delete_app_failedjob(job_id):
  122. myLogger.info_logger("delete_app_failedjob")
  123. failed = FailedJobRegistry(queue=q)
  124. failed.remove(job_id, delete_job=True)
  125. def uninstall_app(app_id):
  126. code, message = docker.check_app_id(app_id)
  127. if code == None:
  128. app_name = app_id.split('_')[0]
  129. info, code_exist = app_exits_in_docker(app_id)
  130. if code_exist:
  131. app_path = info.split()[-1].rsplit('/', 1)[0]
  132. cmd = "docker compose -f " + app_path + "/docker-compose.yml down -v"
  133. lib_path = '/data/library/apps/' + app_name
  134. if app_path != lib_path:
  135. cmd = cmd + " && sudo rm -rf " + app_path
  136. shell_execute.execute_command_output_all(cmd)
  137. else:
  138. if check_app_rq(app_id):
  139. delete_app_failedjob(app_id)
  140. else:
  141. raise CommandException(const.ERROR_CLIENT_PARAM_NOTEXIST, "AppID is not exist", "")
  142. else:
  143. raise CommandException(code, message, "")
  144. def check_app(app_name, customer_name, app_version):
  145. message = ""
  146. code = None
  147. app_id = app_name + "_" + customer_name
  148. if app_name == None:
  149. code = const.ERROR_CLIENT_PARAM_BLANK
  150. message = "app_name is null"
  151. elif customer_name == None:
  152. code = const.ERROR_CLIENT_PARAM_BLANK
  153. message = "customer_name is null"
  154. elif app_version == None:
  155. code = const.ERROR_CLIENT_PARAM_BLANK
  156. message = "app_version is null"
  157. elif not docker.check_app_websoft9(app_name):
  158. code = const.ERROR_CLIENT_PARAM_NOTEXIST
  159. message = "It is not support to install " + app_name
  160. elif re.match('^[a-z0-9]+$', customer_name) == None:
  161. code = const.ERROR_CLIENT_PARAM_Format
  162. message = "APP name can only be composed of numbers and lowercase letters"
  163. elif docker.check_directory("/data/apps/" + customer_name):
  164. code = const.ERROR_CLIENT_PARAM_REPEAT
  165. message = "Repeat installation: " + customer_name
  166. elif not docker.check_vm_resource(app_name):
  167. code = const.ERROR_SERVER_RESOURCE
  168. message = "Insufficient system resources (cpu, memory, disk space)"
  169. elif check_app_docker(app_id):
  170. code = const.ERROR_CLIENT_PARAM_REPEAT
  171. message = "Repeat installation: " + customer_name
  172. elif check_app_rq(app_id):
  173. code = const.ERROR_CLIENT_PARAM_REPEAT
  174. message = "Repeat installation: " + customer_name
  175. return code, message
  176. def prepare_app(app_name, customer_name):
  177. library_path = "/data/library/apps/" + app_name
  178. install_path = "/data/apps/" + customer_name
  179. shell_execute.execute_command_output_all("cp -r " + library_path + " " + install_path)
  180. def install_app_delay(app_name, customer_name, app_version):
  181. myLogger.info_logger("-------RQ install start --------")
  182. job_id = app_name + "_" + customer_name
  183. try:
  184. # 因为这个时候还没有复制文件夹,是从/data/library里面文件读取json来检查的,应该是app_name,而不是customer_name
  185. resource_flag = docker.check_vm_resource(app_name)
  186. if resource_flag == True:
  187. myLogger.info_logger("job check ok, continue to install app")
  188. env_path = "/data/apps/" + customer_name + "/.env"
  189. # prepare_app(app_name, customer_name)
  190. docker.check_app_compose(app_name, customer_name)
  191. myLogger.info_logger("start JobID=" + job_id)
  192. docker.modify_env(env_path, 'APP_NAME', customer_name)
  193. docker.modify_env(env_path, "APP_VERSION", app_version)
  194. cmd = "cd /data/apps/" + customer_name + " && sudo docker compose pull && sudo docker compose up -d"
  195. output = shell_execute.execute_command_output_all(cmd)
  196. myLogger.info_logger("-------Install result--------")
  197. myLogger.info_logger(output["code"])
  198. myLogger.info_logger(output["result"])
  199. else:
  200. error_info= "##websoft9##" + const.ERROR_SERVER_RESOURCE + "##websoft9##" + "Insufficient system resources (cpu, memory, disk space)" + "##websoft9##" + "Insufficient system resources (cpu, memory, disk space)"
  201. myLogger.info_logger(error_info)
  202. raise Exception(error_info)
  203. except CommandException as ce:
  204. myLogger.info_logger(customer_name + " install failed(docker)!")
  205. uninstall_app(job_id)
  206. error_info= "##websoft9##" + ce.code + "##websoft9##" + ce.message + "##websoft9##" + ce.detail
  207. myLogger.info_logger(error_info)
  208. raise Exception(error_info)
  209. except Exception as e:
  210. myLogger.info_logger(customer_name + " install failed(system)!")
  211. uninstall_app(job_id)
  212. error_info= "##websoft9##" + const.ERROR_SERVER_SYSTEM + "##websoft9##" + 'system original error' + "##websoft9##" + str(e)
  213. myLogger.info_logger(error_info)
  214. raise Exception(error_info)
  215. def app_exits_in_docker(app_id):
  216. customer_name = app_id.split('_')[1]
  217. app_name = app_id.split('_')[0]
  218. flag = False
  219. info = ""
  220. cmd = "docker compose ls -a | grep \'/" + customer_name + "/\'"
  221. try:
  222. output = shell_execute.execute_command_output_all(cmd)
  223. if int(output["code"]) == 0:
  224. info = output["result"]
  225. app_path = info.split()[-1].rsplit('/', 1)[0]
  226. is_official = check_if_official_app(app_path + '/variables.json')
  227. if is_official:
  228. name = docker.read_var(app_path + '/variables.json', 'name')
  229. if name == app_name:
  230. flag = True
  231. elif app_name == customer_name:
  232. flag = True
  233. myLogger.info_logger("APP in docker")
  234. except CommandException as ce:
  235. myLogger.info_logger("APP not in docker")
  236. return info, flag
  237. def split_app_id(app_id):
  238. return app_id.split("_")[1]
  239. def get_apps_from_compose():
  240. myLogger.info_logger("Search all of apps ...")
  241. cmd = "docker compose ls -a --format json"
  242. output = shell_execute.execute_command_output_all(cmd)
  243. output_list = json.loads(output["result"])
  244. myLogger.info_logger(len(output_list))
  245. ip = "localhost"
  246. try:
  247. ip_result = shell_execute.execute_command_output_all("cat /data/apps/stackhub/docker/w9appmanage/public_ip")
  248. ip = ip_result["result"].rstrip('\n')
  249. except Exception:
  250. ip = "127.0.0.1"
  251. app_list = []
  252. for app_info in output_list:
  253. volume = app_info["ConfigFiles"] # volume
  254. app_path = volume.rsplit('/', 1)[0]
  255. customer_name = volume.split('/')[-2]
  256. app_id = "" # app_id
  257. app_name = ""
  258. trade_mark = ""
  259. port = 0
  260. url = ""
  261. admin_url = ""
  262. image_url = ""
  263. user_name = ""
  264. password = ""
  265. official_app = False
  266. if customer_name in ['w9appmanage', 'w9nginxproxymanager','w9redis','w9portainer'] and app_path == '/data/apps/stackhub/docker/' + customer_name:
  267. continue
  268. # get code
  269. status = app_info["Status"].split("(")[0]
  270. if status == "running" or status == "exited" or status == "restarting":
  271. myLogger.info_logger("ok")
  272. elif status == "created":
  273. status = "failed"
  274. else:
  275. continue
  276. var_path = app_path + "/variables.json"
  277. official_app = check_if_official_app(var_path)
  278. if official_app:
  279. app_name = docker.read_var(var_path, 'name')
  280. app_id = app_name + "_" + customer_name # app_id
  281. # get trade_mark
  282. trade_mark = docker.read_var(var_path, 'trademark')
  283. image_url = get_Image_url(app_name)
  284. # get env info
  285. path = app_path + "/.env"
  286. # get port and url
  287. try:
  288. http_port = list(docker.read_env(
  289. path, "APP_HTTP_PORT").values())[0]
  290. port = int(http_port)
  291. easy_url = "http://" + ip + ":" + str(port)
  292. url = get_url(app_name, easy_url)
  293. admin_url = get_admin_url(app_name, url)
  294. except IndexError:
  295. try:
  296. db_port = list(docker.read_env(path, "APP_DB.*_PORT").values())[0]
  297. port = int(db_port)
  298. except IndexError:
  299. pass
  300. # get user_name
  301. try:
  302. user_name = list(docker.read_env(path, "APP_USER").values())[0]
  303. except IndexError:
  304. pass
  305. # get password
  306. try:
  307. password = list(docker.read_env(path, "POWER_PASSWORD").values())[0]
  308. except IndexError:
  309. pass
  310. if status in ['running', 'exited']:
  311. config = Config(port=port, compose_file=volume, url=url, admin_url=admin_url,
  312. user_name=user_name, password=password, default_domain="", set_domain="")
  313. else:
  314. config = None
  315. if status == "failed":
  316. status_reason = StatusReason(Code=const.ERROR_SERVER_SYSTEM, Message="system original error", Detail="unknown error")
  317. else:
  318. status_reason = None
  319. app = App(app_id=app_id, app_name=app_name, customer_name=customer_name, trade_mark=trade_mark, status=status,
  320. status_reason=status_reason, official_app=official_app, image_url=image_url,
  321. config=config)
  322. app_list.append(app.dict())
  323. return app_list
  324. def check_if_official_app(var_path):
  325. if docker.check_directory(var_path):
  326. if docker.read_var(var_path, 'name') != "" and docker.read_var(var_path, 'trademark') != "" and docker.read_var(
  327. var_path, 'requirements') != "":
  328. requirements = docker.read_var(var_path, 'requirements')
  329. try:
  330. cpu = requirements['cpu']
  331. mem = requirements['memory']
  332. disk = requirements['disk']
  333. return True
  334. except KeyError:
  335. return False
  336. else:
  337. return False
  338. def check_app_docker(app_id):
  339. customer_name = app_id.split('_')[1]
  340. app_name = app_id.split('_')[0]
  341. flag = False
  342. cmd = "docker compose ls -a | grep \'/" + customer_name + "/\'"
  343. try:
  344. shell_execute.execute_command_output_all(cmd)
  345. flag = True
  346. myLogger.info_logger("APP in docker")
  347. except CommandException as ce:
  348. myLogger.info_logger("APP not in docker")
  349. return flag
  350. def check_app_rq(app_id):
  351. myLogger.info_logger("check_app_rq")
  352. started = StartedJobRegistry(queue=q)
  353. failed = FailedJobRegistry(queue=q)
  354. run_job_ids = started.get_job_ids()
  355. failed_job_ids = failed.get_job_ids()
  356. queue_job_ids = q.job_ids
  357. myLogger.info_logger(queue_job_ids)
  358. myLogger.info_logger(run_job_ids)
  359. myLogger.info_logger(failed_job_ids)
  360. if queue_job_ids and app_id in queue_job_ids:
  361. myLogger.info_logger("App in RQ")
  362. return True
  363. if failed_job_ids and app_id in failed_job_ids:
  364. myLogger.info_logger("App in RQ")
  365. return True
  366. if run_job_ids and app_id in run_job_ids:
  367. myLogger.info_logger("App in RQ")
  368. return True
  369. myLogger.info_logger("App not in RQ")
  370. return False
  371. def get_apps_from_queue():
  372. myLogger.info_logger("get queque apps...")
  373. # 获取 StartedJobRegistry 实例
  374. started = StartedJobRegistry(queue=q)
  375. finish = FinishedJobRegistry(queue=q)
  376. deferred = DeferredJobRegistry(queue=q)
  377. failed = FailedJobRegistry(queue=q)
  378. scheduled = ScheduledJobRegistry(queue=q)
  379. cancel = CanceledJobRegistry(queue=q)
  380. # 获取正在执行的作业 ID 列表
  381. run_job_ids = started.get_job_ids()
  382. finish_job_ids = finish.get_job_ids()
  383. wait_job_ids = deferred.get_job_ids()
  384. failed_jobs = failed.get_job_ids()
  385. scheduled_jobs = scheduled.get_job_ids()
  386. cancel_jobs = cancel.get_job_ids()
  387. myLogger.info_logger(q.jobs)
  388. myLogger.info_logger(run_job_ids)
  389. myLogger.info_logger(failed_jobs)
  390. myLogger.info_logger(cancel_jobs)
  391. myLogger.info_logger(wait_job_ids)
  392. myLogger.info_logger(finish_job_ids)
  393. myLogger.info_logger(scheduled_jobs)
  394. installing_list = []
  395. for job_id in run_job_ids:
  396. app = get_rq_app(job_id, 'installing', "", "", "")
  397. installing_list.append(app)
  398. for job in q.jobs:
  399. app = get_rq_app(job.id, 'installing', "", "", "")
  400. installing_list.append(app)
  401. for job_id in failed_jobs:
  402. job = q.fetch_job(job_id)
  403. exc_info = job.exc_info
  404. code = exc_info.split('##websoft9##')[1]
  405. message = exc_info.split('##websoft9##')[2]
  406. detail = exc_info.split('##websoft9##')[3]
  407. app = get_rq_app(job_id, 'failed', code, message, detail)
  408. installing_list.append(app)
  409. return installing_list
  410. def get_rq_app(id, status, code, message, detail):
  411. app_name = id.split('_')[0]
  412. customer_name = id.split('_')[1]
  413. # 当app还在RQ时,可能文件夹还没创建,无法获取trade_mark
  414. trade_mark = ""
  415. image_url = get_Image_url(app_name)
  416. config = None
  417. if status == "installing" :
  418. status_reason = None
  419. else:
  420. status_reason = StatusReason(Code=code, Message=message, Detail=detail)
  421. app = App(app_id=id, app_name=app_name, customer_name=customer_name, trade_mark=trade_mark,
  422. status=status, status_reason=status_reason, official_app=True, image_url=image_url,
  423. config=config)
  424. return app.dict()
  425. def get_Image_url(app_name):
  426. image_url = "static/images/" + app_name + "-websoft9.png"
  427. return image_url
  428. def get_url(app_name, easy_url):
  429. url = easy_url
  430. if app_name == "joomla":
  431. url = easy_url + "/administrator"
  432. elif app_name == "other":
  433. url = easy_url + "/administrator"
  434. else:
  435. url = easy_url
  436. return url
  437. def get_admin_url(app_name, url):
  438. admin_url = ""
  439. if app_name == "wordpress":
  440. admin_url = url + "/wp-admin"
  441. elif app_name == "other":
  442. admin_url = url + "/admin"
  443. else:
  444. admin_url = ""
  445. return admin_url
  446. def get_error_info(code, message, detail):
  447. error = {}
  448. error['Code'] = code
  449. error['Message'] = message
  450. error['Detail'] = detail
  451. return error