Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

499 Zeilen
13 KiB

  1. # imports - standard imports
  2. import subprocess
  3. from functools import lru_cache
  4. import os
  5. import shutil
  6. import json
  7. import sys
  8. import logging
  9. from typing import List, MutableSequence, TYPE_CHECKING, Union
  10. # imports - module imports
  11. import bench
  12. from bench.exceptions import AppNotInstalledError, InvalidRemoteException
  13. from bench.config.common_site_config import setup_config
  14. from bench.utils import (
  15. UNSET_ARG,
  16. paths_in_bench,
  17. exec_cmd,
  18. is_bench_directory,
  19. is_xhiveframework_app,
  20. get_cmd_output,
  21. get_git_version,
  22. log,
  23. run_xhiveframework_cmd,
  24. )
  25. from bench.utils.bench import (
  26. validate_app_installed_on_sites,
  27. restart_supervisor_processes,
  28. restart_systemd_processes,
  29. restart_process_manager,
  30. remove_backups_crontab,
  31. get_venv_path,
  32. get_env_cmd,
  33. )
  34. from bench.utils.render import job, step
  35. from bench.utils.app import get_current_version
  36. from bench.app import is_git_repo
  37. if TYPE_CHECKING:
  38. from bench.app import App
  39. logger = logging.getLogger(bench.PROJECT_NAME)
  40. class Base:
  41. def run(self, cmd, cwd=None, _raise=True):
  42. return exec_cmd(cmd, cwd=cwd or self.cwd, _raise=_raise)
  43. class Validator:
  44. def validate_app_uninstall(self, app):
  45. if app not in self.apps:
  46. raise AppNotInstalledError(f"No app named {app}")
  47. validate_app_installed_on_sites(app, bench_path=self.name)
  48. @lru_cache(maxsize=None)
  49. class Bench(Base, Validator):
  50. def __init__(self, path):
  51. self.name = path
  52. self.cwd = os.path.abspath(path)
  53. self.exists = is_bench_directory(self.name)
  54. self.setup = BenchSetup(self)
  55. self.teardown = BenchTearDown(self)
  56. self.apps = BenchApps(self)
  57. self.apps_txt = os.path.join(self.name, "sites", "apps.txt")
  58. self.excluded_apps_txt = os.path.join(self.name, "sites", "excluded_apps.txt")
  59. @property
  60. def python(self) -> str:
  61. return get_env_cmd("python", bench_path=self.name)
  62. @property
  63. def shallow_clone(self) -> bool:
  64. config = self.conf
  65. if config:
  66. if config.get("release_bench") or not config.get("shallow_clone"):
  67. return False
  68. return get_git_version() > 1.9
  69. @property
  70. def excluded_apps(self) -> List:
  71. try:
  72. with open(self.excluded_apps_txt) as f:
  73. return f.read().strip().split("\n")
  74. except Exception:
  75. return []
  76. @property
  77. def sites(self) -> List:
  78. return [
  79. path
  80. for path in os.listdir(os.path.join(self.name, "sites"))
  81. if os.path.exists(os.path.join("sites", path, "site_config.json"))
  82. ]
  83. @property
  84. def conf(self):
  85. from bench.config.common_site_config import get_config
  86. return get_config(self.name)
  87. def init(self):
  88. self.setup.dirs()
  89. self.setup.env()
  90. self.setup.backups()
  91. def drop(self):
  92. self.teardown.backups()
  93. self.teardown.dirs()
  94. def install(self, app, branch=None):
  95. from bench.app import App
  96. app = App(app, branch=branch)
  97. self.apps.append(app)
  98. self.apps.sync()
  99. def uninstall(self, app, no_backup=False, force=False):
  100. from bench.app import App
  101. if not force:
  102. self.validate_app_uninstall(app)
  103. try:
  104. self.apps.remove(App(app, bench=self, to_clone=False), no_backup=no_backup)
  105. except InvalidRemoteException:
  106. if not force:
  107. raise
  108. self.apps.sync()
  109. # self.build() - removed because it seems unnecessary
  110. self.reload(_raise=False)
  111. @step(title="Building Bench Assets", success="Bench Assets Built")
  112. def build(self):
  113. # build assets & stuff
  114. run_xhiveframework_cmd("build", bench_path=self.name)
  115. @step(title="Reloading Bench Processes", success="Bench Processes Reloaded")
  116. def reload(self, web=False, supervisor=True, systemd=True, _raise=True):
  117. """If web is True, only web workers are restarted"""
  118. conf = self.conf
  119. if conf.get("developer_mode"):
  120. restart_process_manager(bench_path=self.name, web_workers=web)
  121. if supervisor or conf.get("restart_supervisor_on_update"):
  122. restart_supervisor_processes(bench_path=self.name, web_workers=web, _raise=_raise)
  123. if systemd and conf.get("restart_systemd_on_update"):
  124. restart_systemd_processes(bench_path=self.name, web_workers=web, _raise=_raise)
  125. def get_installed_apps(self) -> List:
  126. """Returns list of installed apps on bench, not in excluded_apps.txt"""
  127. try:
  128. installed_packages = get_cmd_output(f"{self.python} -m pip freeze", cwd=self.name)
  129. except Exception:
  130. installed_packages = []
  131. return [
  132. app
  133. for app in self.apps
  134. if app not in self.excluded_apps and app in installed_packages
  135. ]
  136. class BenchApps(MutableSequence):
  137. def __init__(self, bench: Bench):
  138. self.bench = bench
  139. self.states_path = os.path.join(self.bench.name, "sites", "apps.json")
  140. self.apps_path = os.path.join(self.bench.name, "apps")
  141. self.initialize_apps()
  142. self.set_states()
  143. def set_states(self):
  144. try:
  145. with open(self.states_path) as f:
  146. self.states = json.loads(f.read() or "{}")
  147. except FileNotFoundError:
  148. self.states = {}
  149. def update_apps_states(
  150. self,
  151. app_dir: str = None,
  152. app_name: Union[str, None] = None,
  153. branch: Union[str, None] = None,
  154. required: List = UNSET_ARG,
  155. ):
  156. if required == UNSET_ARG:
  157. required = []
  158. if self.apps and not os.path.exists(self.states_path):
  159. # idx according to apps listed in apps.txt (backwards compatibility)
  160. # Keeping xhiveframework as the first app.
  161. if "xhiveframework" in self.apps:
  162. self.apps.remove("xhiveframework")
  163. self.apps.insert(0, "xhiveframework")
  164. with open(self.bench.apps_txt, "w") as f:
  165. f.write("\n".join(self.apps))
  166. print("Found existing apps updating states...")
  167. for idx, app in enumerate(self.apps, start=1):
  168. self.states[app] = {
  169. "resolution": {"commit_hash": None, "branch": None},
  170. "required": required,
  171. "idx": idx,
  172. "version": get_current_version(app, self.bench.name),
  173. }
  174. apps_to_remove = []
  175. for app in self.states:
  176. if app not in self.apps:
  177. apps_to_remove.append(app)
  178. for app in apps_to_remove:
  179. del self.states[app]
  180. if app_name and not app_dir:
  181. app_dir = app_name
  182. if app_name and app_name not in self.states:
  183. version = get_current_version(app_name, self.bench.name)
  184. app_dir = os.path.join(self.apps_path, app_dir)
  185. is_repo = is_git_repo(app_dir)
  186. if is_repo:
  187. if not branch:
  188. branch = (
  189. subprocess.check_output(
  190. "git rev-parse --abbrev-ref HEAD", shell=True, cwd=app_dir
  191. )
  192. .decode("utf-8")
  193. .rstrip()
  194. )
  195. commit_hash = (
  196. subprocess.check_output(f"git rev-parse {branch}", shell=True, cwd=app_dir)
  197. .decode("utf-8")
  198. .rstrip()
  199. )
  200. self.states[app_name] = {
  201. "is_repo": is_repo,
  202. "resolution": "not a repo"
  203. if not is_repo
  204. else {"commit_hash": commit_hash, "branch": branch},
  205. "required": required,
  206. "idx": len(self.states) + 1,
  207. "version": version,
  208. }
  209. with open(self.states_path, "w") as f:
  210. f.write(json.dumps(self.states, indent=4))
  211. def sync(
  212. self,
  213. app_name: Union[str, None] = None,
  214. app_dir: Union[str, None] = None,
  215. branch: Union[str, None] = None,
  216. required: List = UNSET_ARG,
  217. ):
  218. if required == UNSET_ARG:
  219. required = []
  220. self.initialize_apps()
  221. with open(self.bench.apps_txt, "w") as f:
  222. f.write("\n".join(self.apps))
  223. self.update_apps_states(
  224. app_name=app_name, app_dir=app_dir, branch=branch, required=required
  225. )
  226. def initialize_apps(self):
  227. try:
  228. self.apps = [
  229. x
  230. for x in os.listdir(os.path.join(self.bench.name, "apps"))
  231. if is_xhiveframework_app(os.path.join(self.bench.name, "apps", x))
  232. ]
  233. self.apps.remove("xhiveframework")
  234. self.apps.insert(0, "xhiveframework")
  235. except FileNotFoundError:
  236. self.apps = []
  237. def __getitem__(self, key):
  238. """retrieves an item by its index, key"""
  239. return self.apps[key]
  240. def __setitem__(self, key, value):
  241. """set the item at index, key, to value"""
  242. # should probably not be allowed
  243. # self.apps[key] = value
  244. raise NotImplementedError
  245. def __delitem__(self, key):
  246. """removes the item at index, key"""
  247. # TODO: uninstall and delete app from bench
  248. del self.apps[key]
  249. def __len__(self):
  250. return len(self.apps)
  251. def insert(self, key, value):
  252. """add an item, value, at index, key."""
  253. # TODO: fetch and install app to bench
  254. self.apps.insert(key, value)
  255. def add(self, app: "App"):
  256. app.get()
  257. app.install()
  258. super().append(app.app_name)
  259. self.apps.sort()
  260. def remove(self, app: "App", no_backup: bool = False):
  261. app.uninstall()
  262. app.remove(no_backup=no_backup)
  263. super().remove(app.app_name)
  264. def append(self, app: "App"):
  265. return self.add(app)
  266. def __repr__(self):
  267. return self.__str__()
  268. def __str__(self):
  269. return str([x for x in self.apps])
  270. class BenchSetup(Base):
  271. def __init__(self, bench: Bench):
  272. self.bench = bench
  273. self.cwd = self.bench.cwd
  274. @step(title="Setting Up Directories", success="Directories Set Up")
  275. def dirs(self):
  276. os.makedirs(self.bench.name, exist_ok=True)
  277. for dirname in paths_in_bench:
  278. os.makedirs(os.path.join(self.bench.name, dirname), exist_ok=True)
  279. @step(title="Setting Up Environment", success="Environment Set Up")
  280. def env(self, python="python3"):
  281. """Setup env folder
  282. - create env if not exists
  283. - upgrade env pip
  284. - install xhiveframework python dependencies
  285. """
  286. import bench.cli
  287. import click
  288. verbose = bench.cli.verbose
  289. click.secho("Setting Up Environment", fg="yellow")
  290. xhiveframework = os.path.join(self.bench.name, "apps", "xhiveframework")
  291. quiet_flag = "" if verbose else "--quiet"
  292. if not os.path.exists(self.bench.python):
  293. venv = get_venv_path(verbose=verbose, python=python)
  294. self.run(f"{venv} env", cwd=self.bench.name)
  295. self.pip()
  296. self.wheel()
  297. if os.path.exists(xhiveframework):
  298. self.run(
  299. f"{self.bench.python} -m pip install {quiet_flag} --upgrade -e {xhiveframework}",
  300. cwd=self.bench.name,
  301. )
  302. @step(title="Setting Up Bench Config", success="Bench Config Set Up")
  303. def config(self, redis=True, procfile=True, additional_config=None):
  304. """Setup config folder
  305. - create pids folder
  306. - generate sites/common_site_config.json
  307. """
  308. setup_config(self.bench.name, additional_config=additional_config)
  309. if redis:
  310. from bench.config.redis import generate_config
  311. generate_config(self.bench.name)
  312. if procfile:
  313. from bench.config.procfile import setup_procfile
  314. setup_procfile(self.bench.name, skip_redis=not redis)
  315. @step(title="Updating pip", success="Updated pip")
  316. def pip(self, verbose=False):
  317. """Updates env pip; assumes that env is setup"""
  318. import bench.cli
  319. verbose = bench.cli.verbose or verbose
  320. quiet_flag = "" if verbose else "--quiet"
  321. return self.run(
  322. f"{self.bench.python} -m pip install {quiet_flag} --upgrade pip", cwd=self.bench.name
  323. )
  324. @step(title="Installing wheel", success="Installed wheel")
  325. def wheel(self, verbose=False):
  326. """Wheel is required for building old setup.py packages.
  327. ref: https://github.com/pypa/pip/issues/8559"""
  328. import bench.cli
  329. verbose = bench.cli.verbose or verbose
  330. quiet_flag = "" if verbose else "--quiet"
  331. return self.run(
  332. f"{self.bench.python} -m pip install {quiet_flag} wheel", cwd=self.bench.name
  333. )
  334. def logging(self):
  335. from bench.utils import setup_logging
  336. return setup_logging(bench_path=self.bench.name)
  337. @step(title="Setting Up Bench Patches", success="Bench Patches Set Up")
  338. def patches(self):
  339. shutil.copy(
  340. os.path.join(os.path.dirname(os.path.abspath(__file__)), "patches", "patches.txt"),
  341. os.path.join(self.bench.name, "patches.txt"),
  342. )
  343. @step(title="Setting Up Backups Cronjob", success="Backups Cronjob Set Up")
  344. def backups(self):
  345. # TODO: to something better for logging data? - maybe a wrapper that auto-logs with more context
  346. logger.log("setting up backups")
  347. from crontab import CronTab
  348. bench_dir = os.path.abspath(self.bench.name)
  349. user = self.bench.conf.get("xhiveframework_user")
  350. logfile = os.path.join(bench_dir, "logs", "backup.log")
  351. system_crontab = CronTab(user=user)
  352. backup_command = f"cd {bench_dir} && {sys.argv[0]} --verbose --site all backup"
  353. job_command = f"{backup_command} >> {logfile} 2>&1"
  354. if job_command not in str(system_crontab):
  355. job = system_crontab.new(
  356. command=job_command, comment="bench auto backups set for every 6 hours"
  357. )
  358. job.every(6).hours()
  359. system_crontab.write()
  360. logger.log("backups were set up")
  361. @job(title="Setting Up Bench Dependencies", success="Bench Dependencies Set Up")
  362. def requirements(self, apps=None):
  363. """Install and upgrade specified / all installed apps on given Bench"""
  364. from bench.app import App
  365. apps = apps or self.bench.apps
  366. self.pip()
  367. print(f"Installing {len(apps)} applications...")
  368. for app in apps:
  369. path_to_app = os.path.join(self.bench.name, "apps", app)
  370. app = App(path_to_app, bench=self.bench, to_clone=False).install(
  371. skip_assets=True, restart_bench=False, ignore_resolution=True
  372. )
  373. def python(self, apps=None):
  374. """Install and upgrade Python dependencies for specified / all installed apps on given Bench"""
  375. import bench.cli
  376. apps = apps or self.bench.apps
  377. quiet_flag = "" if bench.cli.verbose else "--quiet"
  378. self.pip()
  379. for app in apps:
  380. app_path = os.path.join(self.bench.name, "apps", app)
  381. log(f"\nInstalling python dependencies for {app}", level=3, no_log=True)
  382. self.run(f"{self.bench.python} -m pip install {quiet_flag} --upgrade -e {app_path}")
  383. def node(self, apps=None):
  384. """Install and upgrade Node dependencies for specified / all apps on given Bench"""
  385. from bench.utils.bench import update_node_packages
  386. return update_node_packages(bench_path=self.bench.name, apps=apps)
  387. class BenchTearDown:
  388. def __init__(self, bench):
  389. self.bench = bench
  390. def backups(self):
  391. remove_backups_crontab(self.bench.name)
  392. def dirs(self):
  393. shutil.rmtree(self.bench.name)