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.
 
 
 
 

608 Zeilen
15 KiB

  1. # imports - standard imports
  2. import json
  3. import logging
  4. import os
  5. import re
  6. import subprocess
  7. import sys
  8. from functools import lru_cache
  9. from glob import glob
  10. from pathlib import Path
  11. from shlex import split
  12. from tarfile import TarInfo
  13. from typing import List, Optional, Tuple
  14. # imports - third party imports
  15. import click
  16. # imports - module imports
  17. from bench import PROJECT_NAME, VERSION
  18. from bench.exceptions import (
  19. AppNotInstalledError,
  20. CommandFailedError,
  21. InvalidRemoteException,
  22. )
  23. logger = logging.getLogger(PROJECT_NAME)
  24. paths_in_app = ("hooks.py", "modules.txt", "patches.txt")
  25. paths_in_bench = ("apps", "sites", "config", "logs", "config/pids")
  26. sudoers_file = "/etc/sudoers.d/xhiveframework"
  27. UNSET_ARG = object()
  28. def is_bench_directory(directory=os.path.curdir):
  29. is_bench = True
  30. for folder in paths_in_bench:
  31. path = os.path.abspath(os.path.join(directory, folder))
  32. is_bench = is_bench and os.path.exists(path)
  33. return is_bench
  34. def is_xhiveframework_app(directory: str) -> bool:
  35. is_xhiveframework_app = True
  36. for folder in paths_in_app:
  37. if not is_xhiveframework_app:
  38. break
  39. path = glob(os.path.join(directory, "**", folder))
  40. is_xhiveframework_app = is_xhiveframework_app and path
  41. return bool(is_xhiveframework_app)
  42. def get_bench_cache_path(sub_dir: Optional[str]) -> Path:
  43. relative_path = "~/.cache/bench"
  44. if sub_dir and not sub_dir.startswith("/"):
  45. relative_path += f"/{sub_dir}"
  46. cache_path = os.path.expanduser(relative_path)
  47. cache_path = Path(cache_path)
  48. cache_path.mkdir(parents=True, exist_ok=True)
  49. return cache_path
  50. @lru_cache(maxsize=None)
  51. def is_valid_xhiveframework_branch(xhiveframework_path: str, xhiveframework_branch: str):
  52. """Check if a branch exists in a repo. Throws InvalidRemoteException if branch is not found
  53. Uses native git command to check for branches on a remote.
  54. :param xhiveframework_path: git url
  55. :type xhiveframework_path: str
  56. :param xhiveframework_branch: branch to check
  57. :type xhiveframework_branch: str
  58. :raises InvalidRemoteException: branch for this repo doesn't exist
  59. """
  60. from git.cmd import Git
  61. from git.exc import GitCommandError
  62. g = Git()
  63. if xhiveframework_branch:
  64. try:
  65. res = g.ls_remote("--heads", "--tags", xhiveframework_path, xhiveframework_branch)
  66. if not res:
  67. raise InvalidRemoteException(
  68. f"Invalid branch or tag: {xhiveframework_branch} for the remote {xhiveframework_path}"
  69. )
  70. except GitCommandError as e:
  71. raise InvalidRemoteException(f"Invalid xhiveframework path: {xhiveframework_path}") from e
  72. def log(message, level=0, no_log=False, stderr=False):
  73. import bench
  74. import bench.cli
  75. levels = {
  76. 0: ("blue", "INFO"), # normal
  77. 1: ("green", "SUCCESS"), # success
  78. 2: ("red", "ERROR"), # fail
  79. 3: ("yellow", "WARN"), # warn/suggest
  80. }
  81. color, prefix = levels.get(level, levels[0])
  82. if bench.cli.from_command_line and bench.cli.dynamic_feed:
  83. bench.LOG_BUFFER.append({"prefix": prefix, "message": message, "color": color})
  84. if no_log:
  85. click.secho(message, fg=color, err=stderr)
  86. else:
  87. loggers = {2: logger.error, 3: logger.warning}
  88. level_logger = loggers.get(level, logger.info)
  89. level_logger(message)
  90. click.secho(f"{prefix}: {message}", fg=color, err=stderr)
  91. def check_latest_version():
  92. if VERSION.endswith("dev"):
  93. return
  94. import requests
  95. from semantic_version import Version
  96. try:
  97. pypi_request = requests.get("https://pypi.org/pypi/xhiveframework-bench/json")
  98. except Exception:
  99. # Exceptions thrown are defined in requests.exceptions
  100. # ignore checking on all Exceptions
  101. return
  102. if pypi_request.status_code == 200:
  103. pypi_version_str = pypi_request.json().get("info").get("version")
  104. pypi_version = Version(pypi_version_str)
  105. local_version = Version(VERSION)
  106. if pypi_version > local_version:
  107. log(
  108. f"A newer version of bench is available: {local_version} → {pypi_version}",
  109. stderr=True,
  110. )
  111. def pause_exec(seconds=10):
  112. from time import sleep
  113. for i in range(seconds, 0, -1):
  114. print(f"Will continue execution in {i} seconds...", end="\r")
  115. sleep(1)
  116. print(" " * 40, end="\r")
  117. def exec_cmd(cmd, cwd=".", env=None, _raise=True):
  118. if env:
  119. env.update(os.environ.copy())
  120. click.secho(f"$ {cmd}", fg="bright_black")
  121. cwd_info = f"cd {cwd} && " if cwd != "." else ""
  122. cmd_log = f"{cwd_info}{cmd}"
  123. logger.debug(cmd_log)
  124. spl_cmd = split(cmd)
  125. return_code = subprocess.call(spl_cmd, cwd=cwd, universal_newlines=True, env=env)
  126. if return_code:
  127. logger.warning(f"{cmd_log} executed with exit code {return_code}")
  128. if _raise:
  129. raise CommandFailedError(cmd) from subprocess.CalledProcessError(return_code, cmd)
  130. return return_code
  131. def which(executable: str, raise_err: bool = False) -> str:
  132. from shutil import which
  133. exec_ = which(executable)
  134. if not exec_ and raise_err:
  135. raise FileNotFoundError(f"{executable} not found in PATH")
  136. return exec_
  137. def setup_logging(bench_path=".") -> logging.Logger:
  138. LOG_LEVEL = 15
  139. logging.addLevelName(LOG_LEVEL, "LOG")
  140. def logv(self, message, *args, **kws):
  141. if self.isEnabledFor(LOG_LEVEL):
  142. self._log(LOG_LEVEL, message, args, **kws)
  143. logging.Logger.log = logv
  144. if os.path.exists(os.path.join(bench_path, "logs")):
  145. log_file = os.path.join(bench_path, "logs", "bench.log")
  146. hdlr = logging.FileHandler(log_file)
  147. else:
  148. hdlr = logging.NullHandler()
  149. logger = logging.getLogger(PROJECT_NAME)
  150. formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
  151. hdlr.setFormatter(formatter)
  152. logger.addHandler(hdlr)
  153. logger.setLevel(logging.DEBUG)
  154. return logger
  155. def get_process_manager() -> str:
  156. for proc_man in ["honcho", "foreman", "forego"]:
  157. proc_man_path = which(proc_man)
  158. if proc_man_path:
  159. return proc_man_path
  160. def get_git_version() -> float:
  161. """returns git version from `git --version`
  162. extracts version number from string `get version 1.9.1` etc"""
  163. version = get_cmd_output("git --version")
  164. version = version.strip().split()[2]
  165. version = ".".join(version.split(".")[0:2])
  166. return float(version)
  167. def get_cmd_output(cmd, cwd=".", _raise=True):
  168. output = ""
  169. try:
  170. output = subprocess.check_output(
  171. cmd, cwd=cwd, shell=True, stderr=subprocess.PIPE, encoding="utf-8"
  172. ).strip()
  173. except subprocess.CalledProcessError as e:
  174. if e.output:
  175. output = e.output
  176. elif _raise:
  177. raise
  178. return output
  179. def is_root():
  180. return os.getuid() == 0
  181. def run_xhiveframework_cmd(*args, **kwargs):
  182. from bench.cli import from_command_line
  183. from bench.utils.bench import get_env_cmd
  184. bench_path = kwargs.get("bench_path", ".")
  185. f = get_env_cmd("python", bench_path=bench_path)
  186. sites_dir = os.path.join(bench_path, "sites")
  187. is_async = not from_command_line
  188. if is_async:
  189. stderr = stdout = subprocess.PIPE
  190. else:
  191. stderr = stdout = None
  192. p = subprocess.Popen(
  193. (f, "-m", "xhiveframework.utils.bench_helper", "xhiveframework") + args,
  194. cwd=sites_dir,
  195. stdout=stdout,
  196. stderr=stderr,
  197. )
  198. return_code = print_output(p) if is_async else p.wait()
  199. if return_code > 0:
  200. sys.exit(return_code)
  201. def print_output(p):
  202. from select import select
  203. while p.poll() is None:
  204. readx = select([p.stdout.fileno(), p.stderr.fileno()], [], [])[0]
  205. send_buffer = []
  206. for fd in readx:
  207. if fd == p.stdout.fileno():
  208. while 1:
  209. buf = p.stdout.read(1)
  210. if not len(buf):
  211. break
  212. if buf == "\r" or buf == "\n":
  213. send_buffer.append(buf)
  214. log_line("".join(send_buffer), "stdout")
  215. send_buffer = []
  216. else:
  217. send_buffer.append(buf)
  218. if fd == p.stderr.fileno():
  219. log_line(p.stderr.readline(), "stderr")
  220. return p.poll()
  221. def log_line(data, stream):
  222. if stream == "stderr":
  223. return sys.stderr.write(data)
  224. return sys.stdout.write(data)
  225. def get_bench_name(bench_path):
  226. return os.path.basename(os.path.abspath(bench_path))
  227. def set_git_remote_url(git_url, bench_path="."):
  228. "Set app remote git url"
  229. from bench.app import get_repo_dir
  230. from bench.bench import Bench
  231. app = git_url.rsplit("/", 1)[1].rsplit(".", 1)[0]
  232. if app not in Bench(bench_path).apps:
  233. raise AppNotInstalledError(f"No app named {app}")
  234. app_dir = get_repo_dir(app, bench_path=bench_path)
  235. if os.path.exists(os.path.join(app_dir, ".git")):
  236. exec_cmd(f"git remote set-url upstream {git_url}", cwd=app_dir)
  237. def run_playbook(playbook_name, extra_vars=None, tag=None):
  238. import bench
  239. if not which("ansible"):
  240. print(
  241. "Ansible is needed to run this command, please install it using 'pip"
  242. " install ansible'"
  243. )
  244. sys.exit(1)
  245. args = ["ansible-playbook", "-c", "local", playbook_name, "-vvvv"]
  246. if extra_vars:
  247. args.extend(["-e", json.dumps(extra_vars)])
  248. if tag:
  249. args.extend(["-t", tag])
  250. subprocess.check_call(args, cwd=os.path.join(bench.__path__[0], "playbooks"))
  251. def find_benches(directory: str = None) -> List:
  252. if not directory:
  253. directory = os.path.expanduser("~")
  254. elif os.path.exists(directory):
  255. directory = os.path.abspath(directory)
  256. else:
  257. log("Directory doesn't exist", level=2)
  258. sys.exit(1)
  259. if is_bench_directory(directory):
  260. if os.path.curdir == directory:
  261. print("You are in a bench directory!")
  262. else:
  263. print(f"{directory} is a bench directory!")
  264. return
  265. benches = []
  266. try:
  267. sub_directories = os.listdir(directory)
  268. except PermissionError:
  269. return benches
  270. for sub in sub_directories:
  271. sub = os.path.join(directory, sub)
  272. if os.path.isdir(sub) and not os.path.islink(sub):
  273. if is_bench_directory(sub):
  274. print(f"{sub} found!")
  275. benches.append(sub)
  276. else:
  277. benches.extend(find_benches(sub))
  278. return benches
  279. def is_dist_editable(dist: str) -> bool:
  280. """Is distribution an editable install?"""
  281. for path_item in sys.path:
  282. egg_link = os.path.join(path_item, f"{dist}.egg-link")
  283. if os.path.isfile(egg_link):
  284. return True
  285. return False
  286. def find_parent_bench(path: str) -> str:
  287. """Checks if parent directories are benches"""
  288. if is_bench_directory(directory=path):
  289. return path
  290. home_path = os.path.expanduser("~")
  291. root_path = os.path.abspath(os.sep)
  292. if path not in {home_path, root_path}:
  293. # NOTE: the os.path.split assumes that given path is absolute
  294. parent_dir = os.path.split(path)[0]
  295. return find_parent_bench(parent_dir)
  296. def get_env_xhiveframework_commands(bench_path=".") -> List:
  297. """Caches all available commands (even custom apps) via Xhiveframework
  298. Default caching behaviour: generated the first time any command (for a specific bench directory)
  299. """
  300. from bench.utils.bench import get_env_cmd
  301. python = get_env_cmd("python", bench_path=bench_path)
  302. sites_path = os.path.join(bench_path, "sites")
  303. try:
  304. return json.loads(
  305. get_cmd_output(
  306. f"{python} -m xhiveframework.utils.bench_helper get-xhiveframework-commands", cwd=sites_path
  307. )
  308. )
  309. except subprocess.CalledProcessError as e:
  310. if hasattr(e, "stderr"):
  311. print(e.stderr)
  312. return []
  313. def find_org(org_repo, using_cached: bool=False):
  314. import requests
  315. org_repo = org_repo[0]
  316. for org in ["xhiveframework", "xhiveerp"]:
  317. res = requests.head(f"https://api.github.com/repos/{org}/{org_repo}")
  318. if res.status_code in (400, 403):
  319. res = requests.head(f"https://lab.membtech.com/{org}/{org_repo}")
  320. if res.ok:
  321. return org, org_repo
  322. if using_cached:
  323. return "", org_repo
  324. raise InvalidRemoteException(f"{org_repo} not found under xhiveframework or xhiveerp GitHub accounts")
  325. def fetch_details_from_tag(_tag: str, using_cached: bool=False) -> Tuple[str, str, str]:
  326. if not _tag:
  327. raise Exception("Tag is not provided")
  328. app_tag = _tag.split("@")
  329. org_repo = app_tag[0].split("/")
  330. try:
  331. repo, tag = app_tag
  332. except ValueError:
  333. repo, tag = app_tag + [None]
  334. try:
  335. org, repo = org_repo
  336. except Exception:
  337. org, repo = find_org(org_repo, using_cached)
  338. return org, repo, tag
  339. def is_git_url(url: str) -> bool:
  340. # modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git
  341. pattern = r"(?:git|ssh|https?|\w*@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$"
  342. return bool(re.match(pattern, url))
  343. def drop_privileges(uid_name="nobody", gid_name="nogroup"):
  344. import grp
  345. import pwd
  346. # from http://stackoverflow.com/a/2699996
  347. if os.getuid() != 0:
  348. # We're not root so, like, whatever dude
  349. return
  350. # Get the uid/gid from the name
  351. running_uid = pwd.getpwnam(uid_name).pw_uid
  352. running_gid = grp.getgrnam(gid_name).gr_gid
  353. # Remove group privileges
  354. os.setgroups([])
  355. # Try setting the new uid/gid
  356. os.setgid(running_gid)
  357. os.setuid(running_uid)
  358. # Ensure a very conservative umask
  359. os.umask(0o22)
  360. def get_available_folder_name(name: str, path: str) -> str:
  361. """Subfixes the passed name with -1 uptil -100 whatever's available"""
  362. if os.path.exists(os.path.join(path, name)):
  363. for num in range(1, 100):
  364. _dt = f"{name}_{num}"
  365. if not os.path.exists(os.path.join(path, _dt)):
  366. return _dt
  367. return name
  368. def get_traceback() -> str:
  369. """Returns the traceback of the Exception"""
  370. from traceback import format_exception
  371. exc_type, exc_value, exc_tb = sys.exc_info()
  372. if not any([exc_type, exc_value, exc_tb]):
  373. return ""
  374. trace_list = format_exception(exc_type, exc_value, exc_tb)
  375. return "".join(trace_list)
  376. class _dict(dict):
  377. """dict like object that exposes keys as attributes"""
  378. # bench port of xhiveframework._dict
  379. def __getattr__(self, key):
  380. ret = self.get(key)
  381. # "__deepcopy__" exception added to fix xhiveframework#14833 via DFP
  382. if not ret and key.startswith("__") and key != "__deepcopy__":
  383. raise AttributeError()
  384. return ret
  385. def __setattr__(self, key, value):
  386. self[key] = value
  387. def __getstate__(self):
  388. return self
  389. def __setstate__(self, d):
  390. self.update(d)
  391. def update(self, d):
  392. """update and return self -- the missing dict feature in python"""
  393. super().update(d)
  394. return self
  395. def copy(self):
  396. return _dict(dict(self).copy())
  397. def get_cmd_from_sysargv():
  398. """Identify and segregate tokens to options and command
  399. For Command: `bench --profile --site xhiveframework.com migrate --no-backup`
  400. sys.argv: ["/home/xhiveframework/.local/bin/bench", "--profile", "--site", "xhiveframework.com", "migrate", "--no-backup"]
  401. Actual command run: migrate
  402. """
  403. # context is passed as options to xhiveframework's bench_helper
  404. from bench.bench import Bench
  405. xhiveframework_context = _dict(params={"--site"}, flags={"--verbose", "--profile", "--force"})
  406. cmd_from_ctx = None
  407. sys_argv = sys.argv[1:]
  408. skip_next = False
  409. for arg in sys_argv:
  410. if skip_next:
  411. skip_next = False
  412. continue
  413. if arg in xhiveframework_context.flags:
  414. continue
  415. elif arg in xhiveframework_context.params:
  416. skip_next = True
  417. continue
  418. if sys_argv.index(arg) == 0 and arg in Bench(".").apps:
  419. continue
  420. cmd_from_ctx = arg
  421. break
  422. return cmd_from_ctx
  423. def get_app_cache_extract_filter(
  424. count_threshold: int = 10_000,
  425. size_threshold: int = 1_000_000_000,
  426. ): # -> Callable[[TarInfo, str], TarInfo | None]
  427. state = dict(count=0, size=0)
  428. AbsoluteLinkError = Exception
  429. def data_filter(m: TarInfo, _:str) -> TarInfo:
  430. return m
  431. if (sys.version_info.major == 3 and sys.version_info.minor > 7) or sys.version_info.major > 3:
  432. from tarfile import data_filter, AbsoluteLinkError
  433. def filter_function(member: TarInfo, dest_path: str) -> Optional[TarInfo]:
  434. state["count"] += 1
  435. state["size"] += member.size
  436. if state["count"] > count_threshold:
  437. raise RuntimeError(f"Number of entries exceeds threshold ({state['count']})")
  438. if state["size"] > size_threshold:
  439. raise RuntimeError(f"Extracted size exceeds threshold ({state['size']})")
  440. try:
  441. return data_filter(member, dest_path)
  442. except AbsoluteLinkError:
  443. # Links created by `xhiveframework` after extraction
  444. return None
  445. return filter_function