您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 

639 行
19 KiB

  1. # imports - standard imports
  2. import contextlib
  3. import json
  4. import logging
  5. import os
  6. import re
  7. import subprocess
  8. import sys
  9. from functools import lru_cache
  10. from glob import glob
  11. from json.decoder import JSONDecodeError
  12. # imports - third party imports
  13. import click
  14. # imports - module imports
  15. import bench
  16. from bench.exceptions import PatchError, ValidationError
  17. from bench.utils import exec_cmd, get_bench_name, get_cmd_output, log, which
  18. logger = logging.getLogger(bench.PROJECT_NAME)
  19. @lru_cache(maxsize=None)
  20. def get_env_cmd(cmd: str, bench_path: str = ".") -> str:
  21. # this supports envs' generated by patched virtualenv or venv (which may cause an extra 'local' folder to be created)
  22. existing_python_bins = glob(
  23. os.path.join(bench_path, "env", "**", "bin", cmd), recursive=True
  24. )
  25. if existing_python_bins:
  26. return os.path.abspath(existing_python_bins[0])
  27. cmd = cmd.strip("*")
  28. return os.path.abspath(os.path.join(bench_path, "env", "bin", cmd))
  29. def get_venv_path(verbose=False, python="python3"):
  30. with open(os.devnull, "wb") as devnull:
  31. is_venv_installed = not subprocess.call(
  32. [python, "-m", "venv", "--help"], stdout=devnull
  33. )
  34. if is_venv_installed:
  35. return f"{python} -m venv"
  36. else:
  37. log("venv cannot be found", level=2)
  38. def update_node_packages(bench_path=".", apps=None):
  39. print("Updating node packages...")
  40. from distutils.version import LooseVersion
  41. from bench.utils.app import get_develop_version
  42. v = LooseVersion(get_develop_version("xhiveframework", bench_path=bench_path))
  43. # After rollup was merged, xhiveframework_version = 10.1
  44. # if develop_verion is 11 and up, only then install yarn
  45. if v < LooseVersion("11.x.x-develop"):
  46. update_npm_packages(bench_path, apps=apps)
  47. else:
  48. update_yarn_packages(bench_path, apps=apps)
  49. def install_python_dev_dependencies(bench_path=".", apps=None, verbose=False):
  50. import bench.cli
  51. from bench.bench import Bench
  52. verbose = bench.cli.verbose or verbose
  53. quiet_flag = "" if verbose else "--quiet"
  54. bench = Bench(bench_path)
  55. if isinstance(apps, str):
  56. apps = [apps]
  57. elif not apps:
  58. apps = bench.get_installed_apps()
  59. for app in apps:
  60. pyproject_deps = None
  61. app_path = os.path.join(bench_path, "apps", app)
  62. pyproject_path = os.path.join(app_path, "pyproject.toml")
  63. dev_requirements_path = os.path.join(app_path, "dev-requirements.txt")
  64. if os.path.exists(pyproject_path):
  65. pyproject_deps = _generate_dev_deps_pattern(pyproject_path)
  66. if pyproject_deps:
  67. bench.run(f"{bench.python} -m pip install {quiet_flag} --upgrade {pyproject_deps}")
  68. if not pyproject_deps and os.path.exists(dev_requirements_path):
  69. bench.run(
  70. f"{bench.python} -m pip install {quiet_flag} --upgrade -r {dev_requirements_path}"
  71. )
  72. def _generate_dev_deps_pattern(pyproject_path):
  73. try:
  74. from tomli import loads
  75. except ImportError:
  76. from tomllib import loads
  77. requirements_pattern = ""
  78. pyroject_config = loads(open(pyproject_path).read())
  79. with contextlib.suppress(KeyError):
  80. for pkg, version in pyroject_config["tool"]["bench"]["dev-dependencies"].items():
  81. op = "==" if "=" not in version else ""
  82. requirements_pattern += f"{pkg}{op}{version} "
  83. return requirements_pattern
  84. def update_yarn_packages(bench_path=".", apps=None):
  85. from bench.bench import Bench
  86. bench = Bench(bench_path)
  87. apps = apps or bench.apps
  88. apps_dir = os.path.join(bench.name, "apps")
  89. # TODO: Check for stuff like this early on only??
  90. if not which("yarn"):
  91. print("Please install yarn using below command and try again.")
  92. print("`npm install -g yarn`")
  93. return
  94. for app in apps:
  95. app_path = os.path.join(apps_dir, app)
  96. if os.path.exists(os.path.join(app_path, "package.json")):
  97. click.secho(f"\nInstalling node dependencies for {app}", fg="yellow")
  98. bench.run("yarn install", cwd=app_path)
  99. def update_npm_packages(bench_path=".", apps=None):
  100. apps_dir = os.path.join(bench_path, "apps")
  101. package_json = {}
  102. if not apps:
  103. apps = os.listdir(apps_dir)
  104. for app in apps:
  105. package_json_path = os.path.join(apps_dir, app, "package.json")
  106. if os.path.exists(package_json_path):
  107. with open(package_json_path) as f:
  108. app_package_json = json.loads(f.read())
  109. # package.json is usually a dict in a dict
  110. for key, value in app_package_json.items():
  111. if key not in package_json:
  112. package_json[key] = value
  113. else:
  114. if isinstance(value, dict):
  115. package_json[key].update(value)
  116. elif isinstance(value, list):
  117. package_json[key].extend(value)
  118. else:
  119. package_json[key] = value
  120. if package_json is {}:
  121. with open(os.path.join(os.path.dirname(__file__), "package.json")) as f:
  122. package_json = json.loads(f.read())
  123. with open(os.path.join(bench_path, "package.json"), "w") as f:
  124. f.write(json.dumps(package_json, indent=1, sort_keys=True))
  125. exec_cmd("npm install", cwd=bench_path)
  126. def migrate_env(python, backup=False):
  127. import shutil
  128. from urllib.parse import urlparse
  129. from bench.bench import Bench
  130. bench = Bench(".")
  131. nvenv = "env"
  132. path = os.getcwd()
  133. python = which(python)
  134. pvenv = os.path.join(path, nvenv)
  135. if python.startswith(pvenv):
  136. # The supplied python version is in active virtualenv which we are about to nuke.
  137. click.secho(
  138. "Python version supplied is present in currently sourced virtual environment.\n"
  139. "`deactiviate` the current virtual environment before migrating environments.",
  140. fg="yellow",
  141. )
  142. sys.exit(1)
  143. # Clear Cache before Bench Dies.
  144. try:
  145. config = bench.conf
  146. rredis = urlparse(config["redis_cache"])
  147. redis = f"{which('redis-cli')} -p {rredis.port}"
  148. logger.log("Clearing Redis Cache...")
  149. exec_cmd(f"{redis} FLUSHALL")
  150. logger.log("Clearing Redis DataBase...")
  151. exec_cmd(f"{redis} FLUSHDB")
  152. except Exception:
  153. logger.warning("Please ensure Redis Connections are running or Daemonized.")
  154. # Backup venv: restore using `virtualenv --relocatable` if needed
  155. if backup:
  156. from datetime import datetime
  157. parch = os.path.join(path, "archived", "envs")
  158. os.makedirs(parch, exist_ok=True)
  159. source = os.path.join(path, "env")
  160. target = parch
  161. logger.log("Backing up Virtual Environment")
  162. stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  163. dest = os.path.join(path, str(stamp))
  164. os.rename(source, dest)
  165. shutil.move(dest, target)
  166. # Create virtualenv using specified python
  167. def _install_app(app):
  168. app_path = f"-e {os.path.join('apps', app)}"
  169. exec_cmd(f"{pvenv}/bin/python -m pip install --upgrade {app_path}")
  170. try:
  171. logger.log(f"Setting up a New Virtual {python} Environment")
  172. exec_cmd(f"{python} -m venv {pvenv}")
  173. # Install xhiveframework first
  174. _install_app("xhiveframework")
  175. for app in bench.apps:
  176. if str(app) != "xhiveframework":
  177. _install_app(app)
  178. logger.log(f"Migration Successful to {python}")
  179. except Exception:
  180. logger.warning("Python env migration Error", exc_info=True)
  181. raise
  182. def validate_upgrade(from_ver, to_ver, bench_path="."):
  183. if to_ver >= 6 and not which("npm") and not which("node") and not which("nodejs"):
  184. raise Exception("Please install nodejs and npm")
  185. def post_upgrade(from_ver, to_ver, bench_path="."):
  186. from bench.bench import Bench
  187. from bench.config import redis
  188. from bench.config.nginx import make_nginx_conf
  189. from bench.config.supervisor import generate_supervisor_config
  190. conf = Bench(bench_path).conf
  191. print("-" * 80 + f"Your bench was upgraded to version {to_ver}")
  192. if conf.get("restart_supervisor_on_update"):
  193. redis.generate_config(bench_path=bench_path)
  194. generate_supervisor_config(bench_path=bench_path)
  195. make_nginx_conf(bench_path=bench_path)
  196. print(
  197. "As you have setup your bench for production, you will have to reload"
  198. " configuration for nginx and supervisor. To complete the migration, please"
  199. " run the following commands:\nsudo service nginx restart\nsudo"
  200. " supervisorctl reload"
  201. )
  202. def patch_sites(bench_path="."):
  203. from bench.bench import Bench
  204. from bench.utils.system import migrate_site
  205. bench = Bench(bench_path)
  206. for site in bench.sites:
  207. try:
  208. migrate_site(site, bench_path=bench_path)
  209. except subprocess.CalledProcessError:
  210. raise PatchError
  211. def restart_supervisor_processes(bench_path=".", web_workers=False, _raise=False):
  212. from bench.bench import Bench
  213. bench = Bench(bench_path)
  214. conf = bench.conf
  215. cmd = conf.get("supervisor_restart_cmd")
  216. bench_name = get_bench_name(bench_path)
  217. if cmd:
  218. bench.run(cmd, _raise=_raise)
  219. else:
  220. sudo = ""
  221. try:
  222. supervisor_status = get_cmd_output("supervisorctl status", cwd=bench_path)
  223. except subprocess.CalledProcessError as e:
  224. if e.returncode == 127:
  225. log("restart failed: Couldn't find supervisorctl in PATH", level=3)
  226. return
  227. sudo = "sudo "
  228. supervisor_status = get_cmd_output("sudo supervisorctl status", cwd=bench_path)
  229. if not sudo and (
  230. "error: <class 'PermissionError'>, [Errno 13] Permission denied" in supervisor_status
  231. ):
  232. sudo = "sudo "
  233. supervisor_status = get_cmd_output("sudo supervisorctl status", cwd=bench_path)
  234. if web_workers and f"{bench_name}-web:" in supervisor_status:
  235. group = f"{bench_name}-web:\t"
  236. elif f"{bench_name}-workers:" in supervisor_status:
  237. group = f"{bench_name}-workers: {bench_name}-web:"
  238. # backward compatibility
  239. elif f"{bench_name}-processes:" in supervisor_status:
  240. group = f"{bench_name}-processes:"
  241. # backward compatibility
  242. else:
  243. group = "xhiveframework:"
  244. failure = bench.run(f"{sudo}supervisorctl restart {group}", _raise=_raise)
  245. if failure:
  246. log("restarting supervisor failed. Use `bench restart` to retry.", level=3)
  247. def restart_systemd_processes(bench_path=".", web_workers=False, _raise=True):
  248. bench_name = get_bench_name(bench_path)
  249. exec_cmd(
  250. f"sudo systemctl stop -- $(systemctl show -p Requires {bench_name}.target | cut"
  251. " -d= -f2)",
  252. _raise=_raise,
  253. )
  254. exec_cmd(
  255. f"sudo systemctl start -- $(systemctl show -p Requires {bench_name}.target |"
  256. " cut -d= -f2)",
  257. _raise=_raise,
  258. )
  259. def restart_process_manager(bench_path=".", web_workers=False):
  260. # only overmind has the restart feature, not sure other supported procmans do
  261. if which("overmind") and os.path.exists(os.path.join(bench_path, ".overmind.sock")):
  262. worker = "web" if web_workers else ""
  263. exec_cmd(f"overmind restart {worker}", cwd=bench_path)
  264. def build_assets(bench_path=".", app=None):
  265. command = "bench build"
  266. if app:
  267. command += f" --app {app}"
  268. exec_cmd(command, cwd=bench_path, env={"BENCH_DEVELOPER": "1"})
  269. def handle_version_upgrade(version_upgrade, bench_path, force, reset, conf):
  270. from bench.utils import log, pause_exec
  271. if version_upgrade[0]:
  272. if force:
  273. log(
  274. """Force flag has been used for a major version change in Xhive and it's apps.
  275. This will take significant time to migrate and might break custom apps.""",
  276. level=3,
  277. )
  278. else:
  279. print(
  280. f"""This update will cause a major version change in Xhive/XhiveERP from {version_upgrade[1]} to {version_upgrade[2]}.
  281. This would take significant time to migrate and might break custom apps."""
  282. )
  283. click.confirm("Do you want to continue?", abort=True)
  284. if not reset and conf.get("shallow_clone"):
  285. log(
  286. """shallow_clone is set in your bench config.
  287. However without passing the --reset flag, your repositories will be unshallowed.
  288. To avoid this, cancel this operation and run `bench update --reset`.
  289. Consider the consequences of `git reset --hard` on your apps before you run that.
  290. To avoid seeing this warning, set shallow_clone to false in your common_site_config.json
  291. """,
  292. level=3,
  293. )
  294. pause_exec(seconds=10)
  295. if version_upgrade[0] or (not version_upgrade[0] and force):
  296. validate_upgrade(version_upgrade[1], version_upgrade[2], bench_path=bench_path)
  297. def update(
  298. pull: bool = False,
  299. apps: str = None,
  300. patch: bool = False,
  301. build: bool = False,
  302. requirements: bool = False,
  303. backup: bool = True,
  304. compile: bool = True,
  305. force: bool = False,
  306. reset: bool = False,
  307. restart_supervisor: bool = False,
  308. restart_systemd: bool = False,
  309. ):
  310. """command: bench update"""
  311. import re
  312. from bench import patches
  313. from bench.app import pull_apps
  314. from bench.bench import Bench
  315. from bench.config.common_site_config import update_config
  316. from bench.exceptions import CannotUpdateReleaseBench
  317. from bench.utils.app import is_version_upgrade
  318. from bench.utils.system import backup_all_sites
  319. bench_path = os.path.abspath(".")
  320. bench = Bench(bench_path)
  321. patches.run(bench_path=bench_path)
  322. conf = bench.conf
  323. if conf.get("release_bench"):
  324. raise CannotUpdateReleaseBench("Release bench detected, cannot update!")
  325. if not (pull or patch or build or requirements):
  326. pull, patch, build, requirements = True, True, True, True
  327. if apps and pull:
  328. apps = [app.strip() for app in re.split(",| ", apps) if app]
  329. else:
  330. apps = []
  331. validate_branch()
  332. version_upgrade = is_version_upgrade()
  333. handle_version_upgrade(version_upgrade, bench_path, force, reset, conf)
  334. conf.update({"maintenance_mode": 1, "pause_scheduler": 1})
  335. update_config(conf, bench_path=bench_path)
  336. if backup:
  337. print("Backing up sites...")
  338. backup_all_sites(bench_path=bench_path)
  339. if pull:
  340. print("Updating apps source...")
  341. pull_apps(apps=apps, bench_path=bench_path, reset=reset)
  342. if requirements:
  343. print("Setting up requirements...")
  344. bench.setup.requirements()
  345. if patch:
  346. print("Patching sites...")
  347. patch_sites(bench_path=bench_path)
  348. if build:
  349. print("Building assets...")
  350. bench.build()
  351. if version_upgrade[0] or (not version_upgrade[0] and force):
  352. post_upgrade(version_upgrade[1], version_upgrade[2], bench_path=bench_path)
  353. if pull and compile:
  354. from compileall import compile_dir
  355. print("Compiling Python files...")
  356. apps_dir = os.path.join(bench_path, "apps")
  357. compile_dir(apps_dir, quiet=1, rx=re.compile(".*node_modules.*"))
  358. bench.reload(web=False, supervisor=restart_supervisor, systemd=restart_systemd)
  359. conf.update({"maintenance_mode": 0, "pause_scheduler": 0})
  360. update_config(conf, bench_path=bench_path)
  361. print(
  362. "_" * 80 + "\nBench: Deployment tool for Xhive and Xhive Applications"
  363. " (https://xhive.io/bench).\nOpen source depends on your contributions, so do"
  364. " give back by submitting bug reports, patches and fixes and be a part of the"
  365. " community :)"
  366. )
  367. def clone_apps_from(bench_path, clone_from, update_app=True):
  368. from bench.app import install_app
  369. print(f"Copying apps from {clone_from}...")
  370. subprocess.check_output(["cp", "-R", os.path.join(clone_from, "apps"), bench_path])
  371. node_modules_path = os.path.join(clone_from, "node_modules")
  372. if os.path.exists(node_modules_path):
  373. print(f"Copying node_modules from {clone_from}...")
  374. subprocess.check_output(["cp", "-R", node_modules_path, bench_path])
  375. def setup_app(app):
  376. # run git reset --hard in each branch, pull latest updates and install_app
  377. app_path = os.path.join(bench_path, "apps", app)
  378. # remove .egg-ino
  379. subprocess.check_output(["rm", "-rf", app + ".egg-info"], cwd=app_path)
  380. if update_app and os.path.exists(os.path.join(app_path, ".git")):
  381. remotes = subprocess.check_output(["git", "remote"], cwd=app_path).strip().split()
  382. if "upstream" in remotes:
  383. remote = "upstream"
  384. else:
  385. remote = remotes[0]
  386. print(f"Cleaning up {app}")
  387. branch = subprocess.check_output(
  388. ["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=app_path
  389. ).strip()
  390. subprocess.check_output(["git", "reset", "--hard"], cwd=app_path)
  391. subprocess.check_output(["git", "pull", "--rebase", remote, branch], cwd=app_path)
  392. install_app(app, bench_path, restart_bench=False)
  393. with open(os.path.join(clone_from, "sites", "apps.txt")) as f:
  394. apps = f.read().splitlines()
  395. for app in apps:
  396. setup_app(app)
  397. def remove_backups_crontab(bench_path="."):
  398. from crontab import CronTab
  399. from bench.bench import Bench
  400. logger.log("removing backup cronjob")
  401. bench_dir = os.path.abspath(bench_path)
  402. user = Bench(bench_dir).conf.get("xhiveframework_user")
  403. logfile = os.path.join(bench_dir, "logs", "backup.log")
  404. system_crontab = CronTab(user=user)
  405. backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
  406. job_command = f"{backup_command} >> {logfile} 2>&1"
  407. system_crontab.remove_all(command=job_command)
  408. def set_mariadb_host(host, bench_path="."):
  409. update_common_site_config({"db_host": host}, bench_path=bench_path)
  410. def set_redis_cache_host(host, bench_path="."):
  411. update_common_site_config({"redis_cache": f"redis://{host}"}, bench_path=bench_path)
  412. def set_redis_queue_host(host, bench_path="."):
  413. update_common_site_config({"redis_queue": f"redis://{host}"}, bench_path=bench_path)
  414. def set_redis_socketio_host(host, bench_path="."):
  415. update_common_site_config({"redis_socketio": f"redis://{host}"}, bench_path=bench_path)
  416. def update_common_site_config(ddict, bench_path="."):
  417. filename = os.path.join(bench_path, "sites", "common_site_config.json")
  418. if os.path.exists(filename):
  419. with open(filename) as f:
  420. content = json.load(f)
  421. else:
  422. content = {}
  423. content.update(ddict)
  424. with open(filename, "w") as f:
  425. json.dump(content, f, indent=1, sort_keys=True)
  426. def validate_app_installed_on_sites(app, bench_path="."):
  427. print("Checking if app installed on active sites...")
  428. ret = check_app_installed(app, bench_path=bench_path)
  429. if ret is None:
  430. check_app_installed_legacy(app, bench_path=bench_path)
  431. else:
  432. return ret
  433. def check_app_installed(app, bench_path="."):
  434. try:
  435. out = subprocess.check_output(
  436. ["bench", "--site", "all", "list-apps", "--format", "json"],
  437. stderr=open(os.devnull, "wb"),
  438. cwd=bench_path,
  439. ).decode("utf-8")
  440. except subprocess.CalledProcessError:
  441. return None
  442. try:
  443. apps_sites_dict = json.loads(out)
  444. except JSONDecodeError:
  445. return None
  446. for site, apps in apps_sites_dict.items():
  447. if app in apps:
  448. raise ValidationError(f"Cannot remove, app is installed on site: {site}")
  449. def check_app_installed_legacy(app, bench_path="."):
  450. site_path = os.path.join(bench_path, "sites")
  451. for site in os.listdir(site_path):
  452. req_file = os.path.join(site_path, site, "site_config.json")
  453. if os.path.exists(req_file):
  454. out = subprocess.check_output(
  455. ["bench", "--site", site, "list-apps"], cwd=bench_path
  456. ).decode("utf-8")
  457. if re.search(r"\b" + app + r"\b", out):
  458. print(f"Cannot remove, app is installed on site: {site}")
  459. sys.exit(1)
  460. def validate_branch():
  461. from bench.bench import Bench
  462. from bench.utils.app import get_current_branch
  463. apps = Bench(".").apps
  464. installed_apps = set(apps)
  465. check_apps = {"xhiveframework", "xhiveerp"}
  466. intersection_apps = installed_apps.intersection(check_apps)
  467. for app in intersection_apps:
  468. branch = get_current_branch(app)
  469. if branch == "master":
  470. print(
  471. """'master' branch is renamed to 'version-11' since 'version-12' release.
  472. As of January 2020, the following branches are
  473. version Xhive XhiveERP
  474. 11 version-11 version-11
  475. 12 version-12 version-12
  476. 13 version-13 version-13
  477. 14 develop develop
  478. Please switch to new branches to get future updates.
  479. To switch to your required branch, run the following commands: bench switch-to-branch [branch-name]"""
  480. )
  481. sys.exit(1)